package hashstructure import ( "encoding/binary" "fmt" "hash" "hash/fnv" "reflect" ) // ErrNotStringer is returned when there's an error with hash:"string" type ErrNotStringer struct { Field string } // Error implements error for ErrNotStringer func (ens *ErrNotStringer) Error() string { return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) } // 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 } // 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. // // 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{}, opts *HashOptions) (uint64, error) { // 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{ h: opts.Hasher, tag: opts.TagName, zeronil: opts.ZeroNil, } return w.visit(reflect.ValueOf(v), nil) } type walker struct { h hash.Hash64 tag string zeronil 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 } 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 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) } return h, nil case reflect.Struct: parent := v.Interface() var include Includable if impl, ok := parent.(Includable); ok { include = impl } 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 string is set, use the string value if tag == "string" { if impl, ok := innerV.Interface().(fmt.Stringer); ok { innerV = reflect.ValueOf(impl.String()) } else { 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) } } 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 { h = hashUpdateUnordered(h, current) } else { h = hashUpdateOrdered(w.h, h, current) } } 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 } // visitFlag is used as a bitmask for affecting visit behavior type visitFlag uint const ( visitFlagInvalid visitFlag = iota visitFlagSet = iota << 1 )