tailscale/util/deephash/types.go
Joe Tsai 60657ac83f
util/deephash: tighten up SelfHasher API (#11012)
Providing a hash.Block512 is an implementation detail of how deephash
works today, but providing an opaque type with mostly equivalent API
(i.e., HashUint8, HashBytes, etc. methods) is still sensible.
Thus, define a public Hasher type that exposes exactly the API
that an implementation of SelfHasher would want to call.
This gives us freedom to change the hashing algorithm of deephash
at some point in the future.

Also, this type is likely going to be called by types that are
going to memoize their own hash results, we additionally add
a HashSum method to simplify this use case.

Add documentation to SelfHasher on how a type might implement it.

Updates: corp#16409

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-02-01 17:07:41 -08:00

118 lines
3.2 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package deephash
import (
"net/netip"
"reflect"
"time"
)
var (
timeTimeType = reflect.TypeOf((*time.Time)(nil)).Elem()
netipAddrType = reflect.TypeOf((*netip.Addr)(nil)).Elem()
selfHasherType = reflect.TypeOf((*SelfHasher)(nil)).Elem()
)
// typeIsSpecialized reports whether this type has specialized hashing.
// These are never memory hashable and never considered recursive.
func typeIsSpecialized(t reflect.Type) bool {
switch t {
case timeTimeType, netipAddrType:
return true
default:
if t.Kind() != reflect.Pointer && t.Kind() != reflect.Interface {
if t.Implements(selfHasherType) || reflect.PointerTo(t).Implements(selfHasherType) {
return true
}
}
return false
}
}
// typeIsMemHashable reports whether t can be hashed by directly hashing its
// contiguous bytes in memory (e.g. structs with gaps are not mem-hashable).
func typeIsMemHashable(t reflect.Type) bool {
if typeIsSpecialized(t) {
return false
}
if t.Size() == 0 {
return true
}
switch t.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128:
return true
case reflect.Array:
return typeIsMemHashable(t.Elem())
case reflect.Struct:
var sumFieldSize uintptr
for i, numField := 0, t.NumField(); i < numField; i++ {
sf := t.Field(i)
if !typeIsMemHashable(sf.Type) {
return false
}
sumFieldSize += sf.Type.Size()
}
return sumFieldSize == t.Size() // ensure no gaps
}
return false
}
// typeIsRecursive reports whether t has a path back to itself.
// For interfaces, it currently always reports true.
func typeIsRecursive(t reflect.Type) bool {
inStack := map[reflect.Type]bool{}
var visitType func(t reflect.Type) (isRecursiveSoFar bool)
visitType = func(t reflect.Type) (isRecursiveSoFar bool) {
// Check whether we have seen this type before.
if inStack[t] {
return true
}
inStack[t] = true
defer func() {
delete(inStack, t)
}()
// Types with specialized hashing are never considered recursive.
if typeIsSpecialized(t) {
return false
}
// Any type that is memory hashable must not be recursive since
// cycles can only occur if pointers are involved.
if typeIsMemHashable(t) {
return false
}
// Recursively check types that may contain pointers.
switch t.Kind() {
default:
panic("unhandled kind " + t.Kind().String())
case reflect.String, reflect.UnsafePointer, reflect.Func:
return false
case reflect.Interface:
// Assume the worst for now. TODO(bradfitz): in some cases
// we should be able to prove that it's not recursive. Not worth
// it for now.
return true
case reflect.Array, reflect.Chan, reflect.Pointer, reflect.Slice:
return visitType(t.Elem())
case reflect.Map:
return visitType(t.Key()) || visitType(t.Elem())
case reflect.Struct:
for i, numField := 0, t.NumField(); i < numField; i++ {
if visitType(t.Field(i).Type) {
return true
}
}
return false
}
}
return visitType(t)
}