123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- package hashstructure
- import (
- "encoding/binary"
- "fmt"
- "hash"
- "hash/fnv"
- "reflect"
- "time"
- )
- // HashOptions are options that are available for hashing.
- type HashOptions struct {
- // Hasher is the hash function to use. If this isn't set, it will
- // default to FNV.
- Hasher hash.Hash64
- // TagName is the struct tag to look at when hashing the structure.
- // By default this is "hash".
- TagName string
- // ZeroNil is flag determining if nil pointer should be treated equal
- // to a zero value of pointed type. By default this is false.
- ZeroNil bool
- // IgnoreZeroValue is determining if zero value fields should be
- // ignored for hash calculation.
- IgnoreZeroValue bool
- // SlicesAsSets assumes that a `set` tag is always present for slices.
- // Default is false (in which case the tag is used instead)
- SlicesAsSets bool
- // UseStringer will attempt to use fmt.Stringer always. If the struct
- // doesn't implement fmt.Stringer, it'll fall back to trying usual tricks.
- // If this is true, and the "string" tag is also set, the tag takes
- // precedence (meaning that if the type doesn't implement fmt.Stringer, we
- // panic)
- UseStringer bool
- }
- // Format specifies the hashing process used. Different formats typically
- // generate different hashes for the same value and have different properties.
- type Format uint
- const (
- // To disallow the zero value
- formatInvalid Format = iota
- // FormatV1 is the format used in v1.x of this library. This has the
- // downsides noted in issue #18 but allows simultaneous v1/v2 usage.
- FormatV1
- // FormatV2 is the current recommended format and fixes the issues
- // noted in FormatV1.
- FormatV2
- formatMax // so we can easily find the end
- )
- // Hash returns the hash value of an arbitrary value.
- //
- // If opts is nil, then default options will be used. See HashOptions
- // for the default values. The same *HashOptions value cannot be used
- // concurrently. None of the values within a *HashOptions struct are
- // safe to read/write while hashing is being done.
- //
- // The "format" is required and must be one of the format values defined
- // by this library. You should probably just use "FormatV2". This allows
- // generated hashes uses alternate logic to maintain compatibility with
- // older versions.
- //
- // Notes on the value:
- //
- // * Unexported fields on structs are ignored and do not affect the
- // hash value.
- //
- // * Adding an exported field to a struct with the zero value will change
- // the hash value.
- //
- // For structs, the hashing can be controlled using tags. For example:
- //
- // struct {
- // Name string
- // UUID string `hash:"ignore"`
- // }
- //
- // The available tag values are:
- //
- // * "ignore" or "-" - The field will be ignored and not affect the hash code.
- //
- // * "set" - The field will be treated as a set, where ordering doesn't
- // affect the hash code. This only works for slices.
- //
- // * "string" - The field will be hashed as a string, only works when the
- // field implements fmt.Stringer
- //
- func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) {
- // Validate our format
- if format <= formatInvalid || format >= formatMax {
- return 0, &ErrFormat{}
- }
- // Create default options
- if opts == nil {
- opts = &HashOptions{}
- }
- if opts.Hasher == nil {
- opts.Hasher = fnv.New64()
- }
- if opts.TagName == "" {
- opts.TagName = "hash"
- }
- // Reset the hash
- opts.Hasher.Reset()
- // Create our walker and walk the structure
- w := &walker{
- format: format,
- h: opts.Hasher,
- tag: opts.TagName,
- zeronil: opts.ZeroNil,
- ignorezerovalue: opts.IgnoreZeroValue,
- sets: opts.SlicesAsSets,
- stringer: opts.UseStringer,
- }
- return w.visit(reflect.ValueOf(v), nil)
- }
- type walker struct {
- format Format
- h hash.Hash64
- tag string
- zeronil bool
- ignorezerovalue bool
- sets bool
- stringer bool
- }
- type visitOpts struct {
- // Flags are a bitmask of flags to affect behavior of this visit
- Flags visitFlag
- // Information about the struct containing this field
- Struct interface{}
- StructField string
- }
- var timeType = reflect.TypeOf(time.Time{})
- func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
- t := reflect.TypeOf(0)
- // Loop since these can be wrapped in multiple layers of pointers
- // and interfaces.
- for {
- // If we have an interface, dereference it. We have to do this up
- // here because it might be a nil in there and the check below must
- // catch that.
- if v.Kind() == reflect.Interface {
- v = v.Elem()
- continue
- }
- if v.Kind() == reflect.Ptr {
- if w.zeronil {
- t = v.Type().Elem()
- }
- v = reflect.Indirect(v)
- continue
- }
- break
- }
- // If it is nil, treat it like a zero.
- if !v.IsValid() {
- v = reflect.Zero(t)
- }
- // Binary writing can use raw ints, we have to convert to
- // a sized-int, we'll choose the largest...
- switch v.Kind() {
- case reflect.Int:
- v = reflect.ValueOf(int64(v.Int()))
- case reflect.Uint:
- v = reflect.ValueOf(uint64(v.Uint()))
- case reflect.Bool:
- var tmp int8
- if v.Bool() {
- tmp = 1
- }
- v = reflect.ValueOf(tmp)
- }
- k := v.Kind()
- // We can shortcut numeric values by directly binary writing them
- if k >= reflect.Int && k <= reflect.Complex64 {
- // A direct hash calculation
- w.h.Reset()
- err := binary.Write(w.h, binary.LittleEndian, v.Interface())
- return w.h.Sum64(), err
- }
- switch v.Type() {
- case timeType:
- w.h.Reset()
- b, err := v.Interface().(time.Time).MarshalBinary()
- if err != nil {
- return 0, err
- }
- err = binary.Write(w.h, binary.LittleEndian, b)
- return w.h.Sum64(), err
- }
- switch k {
- case reflect.Array:
- var h uint64
- l := v.Len()
- for i := 0; i < l; i++ {
- current, err := w.visit(v.Index(i), nil)
- if err != nil {
- return 0, err
- }
- h = hashUpdateOrdered(w.h, h, current)
- }
- return h, nil
- case reflect.Map:
- var includeMap IncludableMap
- if opts != nil && opts.Struct != nil {
- if v, ok := opts.Struct.(IncludableMap); ok {
- includeMap = v
- }
- }
- // Build the hash for the map. We do this by XOR-ing all the key
- // and value hashes. This makes it deterministic despite ordering.
- var h uint64
- for _, k := range v.MapKeys() {
- v := v.MapIndex(k)
- if includeMap != nil {
- incl, err := includeMap.HashIncludeMap(
- opts.StructField, k.Interface(), v.Interface())
- if err != nil {
- return 0, err
- }
- if !incl {
- continue
- }
- }
- kh, err := w.visit(k, nil)
- if err != nil {
- return 0, err
- }
- vh, err := w.visit(v, nil)
- if err != nil {
- return 0, err
- }
- fieldHash := hashUpdateOrdered(w.h, kh, vh)
- h = hashUpdateUnordered(h, fieldHash)
- }
- if w.format != FormatV1 {
- // Important: read the docs for hashFinishUnordered
- h = hashFinishUnordered(w.h, h)
- }
- return h, nil
- case reflect.Struct:
- parent := v.Interface()
- var include Includable
- if impl, ok := parent.(Includable); ok {
- include = impl
- }
- if impl, ok := parent.(Hashable); ok {
- return impl.Hash()
- }
- // If we can address this value, check if the pointer value
- // implements our interfaces and use that if so.
- if v.CanAddr() {
- vptr := v.Addr()
- parentptr := vptr.Interface()
- if impl, ok := parentptr.(Includable); ok {
- include = impl
- }
- if impl, ok := parentptr.(Hashable); ok {
- return impl.Hash()
- }
- }
- t := v.Type()
- h, err := w.visit(reflect.ValueOf(t.Name()), nil)
- if err != nil {
- return 0, err
- }
- l := v.NumField()
- for i := 0; i < l; i++ {
- if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
- var f visitFlag
- fieldType := t.Field(i)
- if fieldType.PkgPath != "" {
- // Unexported
- continue
- }
- tag := fieldType.Tag.Get(w.tag)
- if tag == "ignore" || tag == "-" {
- // Ignore this field
- continue
- }
- if w.ignorezerovalue {
- if innerV.IsZero() {
- continue
- }
- }
- // if string is set, use the string value
- if tag == "string" || w.stringer {
- if impl, ok := innerV.Interface().(fmt.Stringer); ok {
- innerV = reflect.ValueOf(impl.String())
- } else if tag == "string" {
- // We only show this error if the tag explicitly
- // requests a stringer.
- return 0, &ErrNotStringer{
- Field: v.Type().Field(i).Name,
- }
- }
- }
- // Check if we implement includable and check it
- if include != nil {
- incl, err := include.HashInclude(fieldType.Name, innerV)
- if err != nil {
- return 0, err
- }
- if !incl {
- continue
- }
- }
- switch tag {
- case "set":
- f |= visitFlagSet
- }
- kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil)
- if err != nil {
- return 0, err
- }
- vh, err := w.visit(innerV, &visitOpts{
- Flags: f,
- Struct: parent,
- StructField: fieldType.Name,
- })
- if err != nil {
- return 0, err
- }
- fieldHash := hashUpdateOrdered(w.h, kh, vh)
- h = hashUpdateUnordered(h, fieldHash)
- }
- if w.format != FormatV1 {
- // Important: read the docs for hashFinishUnordered
- h = hashFinishUnordered(w.h, h)
- }
- }
- return h, nil
- case reflect.Slice:
- // We have two behaviors here. If it isn't a set, then we just
- // visit all the elements. If it is a set, then we do a deterministic
- // hash code.
- var h uint64
- var set bool
- if opts != nil {
- set = (opts.Flags & visitFlagSet) != 0
- }
- l := v.Len()
- for i := 0; i < l; i++ {
- current, err := w.visit(v.Index(i), nil)
- if err != nil {
- return 0, err
- }
- if set || w.sets {
- h = hashUpdateUnordered(h, current)
- } else {
- h = hashUpdateOrdered(w.h, h, current)
- }
- }
- if set && w.format != FormatV1 {
- // Important: read the docs for hashFinishUnordered
- h = hashFinishUnordered(w.h, h)
- }
- return h, nil
- case reflect.String:
- // Directly hash
- w.h.Reset()
- _, err := w.h.Write([]byte(v.String()))
- return w.h.Sum64(), err
- default:
- return 0, fmt.Errorf("unknown kind to hash: %s", k)
- }
- }
- func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 {
- // For ordered updates, use a real hash function
- h.Reset()
- // We just panic if the binary writes fail because we are writing
- // an int64 which should never be fail-able.
- e1 := binary.Write(h, binary.LittleEndian, a)
- e2 := binary.Write(h, binary.LittleEndian, b)
- if e1 != nil {
- panic(e1)
- }
- if e2 != nil {
- panic(e2)
- }
- return h.Sum64()
- }
- func hashUpdateUnordered(a, b uint64) uint64 {
- return a ^ b
- }
- // After mixing a group of unique hashes with hashUpdateUnordered, it's always
- // necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered
- // is a simple XOR, and calling hashUpdateUnordered on hashes produced by
- // hashUpdateUnordered can effectively cancel out a previous change to the hash
- // result if the same hash value appears later on. For example, consider:
- //
- // hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) =
- // H("A") ^ H("B")) ^ (H("A") ^ H("C")) =
- // (H("A") ^ H("A")) ^ (H("B") ^ H(C)) =
- // H(B) ^ H(C) =
- // hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C"))
- //
- // hashFinishUnordered "hardens" the result, so that encountering partially
- // overlapping input data later on in a different context won't cancel out.
- func hashFinishUnordered(h hash.Hash64, a uint64) uint64 {
- h.Reset()
- // We just panic if the writes fail
- e1 := binary.Write(h, binary.LittleEndian, a)
- if e1 != nil {
- panic(e1)
- }
- return h.Sum64()
- }
- // visitFlag is used as a bitmask for affecting visit behavior
- type visitFlag uint
- const (
- visitFlagInvalid visitFlag = iota
- visitFlagSet = iota << 1
- )
|