Skip to content
This repository was archived by the owner on Jul 22, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 64 additions & 31 deletions hashstructure.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package hashstructure

import (
"encoding"
"encoding/binary"
"fmt"
"hash"
Expand Down Expand Up @@ -37,6 +38,11 @@ type HashOptions struct {
// precedence (meaning that if the type doesn't implement fmt.Stringer, we
// panic)
UseStringer bool

// UnhashedStructFallback will attempt to make use of the BinaryEncoder and
// Stringer interfaces (in that order) to hash structs that contain no
// exported fields.
UnhashedStructFallback bool
}

// Format specifies the hashing process used. Different formats typically
Expand Down Expand Up @@ -72,29 +78,28 @@ const (
//
// Notes on the value:
//
// * Unexported fields on structs are ignored and do not affect the
// - 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
// - 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"`
// }
// 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.
// - "ignore" or "-" - The field will be ignored and not affect the hash code.
//
// * "string" - The field will be hashed as a string, only works when the
// field implements fmt.Stringer
// - "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 {
Expand All @@ -117,25 +122,27 @@ func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) {

// 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,
format: format,
h: opts.Hasher,
tag: opts.TagName,
zeronil: opts.ZeroNil,
ignorezerovalue: opts.IgnoreZeroValue,
sets: opts.SlicesAsSets,
stringer: opts.UseStringer,
unhashedstructfallback: opts.UnhashedStructFallback,
}
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
format Format
h hash.Hash64
tag string
zeronil bool
ignorezerovalue bool
sets bool
stringer bool
unhashedstructfallback bool
}

type visitOpts struct {
Expand Down Expand Up @@ -307,23 +314,27 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
}

l := v.NumField()
unhashedfields := 0
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 != "" {
unhashedfields++
// Unexported
continue
}

tag := fieldType.Tag.Get(w.tag)
if tag == "ignore" || tag == "-" {
unhashedfields++
// Ignore this field
continue
}

if w.ignorezerovalue {
if innerV.IsZero() {
unhashedfields++
continue
}
}
Expand All @@ -348,6 +359,7 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
return 0, err
}
if !incl {
unhashedfields++
continue
}
}
Expand Down Expand Up @@ -380,6 +392,27 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
h = hashFinishUnordered(w.h, h)
}
}
// no fields involved in the hash! try binary and string instead.
if unhashedfields == l && w.unhashedstructfallback {
var data []byte
if impl, ok := parent.(encoding.BinaryMarshaler); ok {
data, err = impl.MarshalBinary()
if err != nil {
return 0, err
}
}

if impl, ok := parent.(fmt.Stringer); ok {
data = []byte(impl.String())
}

w.h.Reset()
_, err := w.h.Write(data)
if err != nil {
return 0, err
}
return w.h.Sum64(), nil
}

return h, nil

Expand Down Expand Up @@ -453,11 +486,11 @@ func hashUpdateUnordered(a, b uint64) uint64 {
// 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"))
// 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.
Expand Down
132 changes: 132 additions & 0 deletions hashstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,3 +729,135 @@ func (t *testHashablePointer) Hash() (uint64, error) {

return 100, nil
}

type UnexportedStringer struct {
n int
}

func (u UnexportedStringer) String() string {
return fmt.Sprintf("%d", u.n)
}

type UnexportedBinaryer struct {
n int
}

func (u UnexportedBinaryer) MarshalBinary() (data []byte, err error) {
return []byte(fmt.Sprintf("%d", u.n)), nil
}

func TestHash_StringIgnoredStructs(t *testing.T) {
cases := []struct {
One, Two interface{}
Match bool
Err string
}{
{
UnexportedStringer{n: 1},
UnexportedStringer{n: 1},
true,
"",
},
{
UnexportedStringer{n: 1},
UnexportedStringer{n: 2},
false,
"",
},
{
[]interface{}{UnexportedStringer{n: 1}},
[]interface{}{UnexportedStringer{n: 1}},
true,
"",
},
{
[]interface{}{UnexportedStringer{n: 1}},
[]interface{}{UnexportedStringer{n: 2}},
false,
"",
},
{
map[string]interface{}{"v": UnexportedStringer{n: 1}},
map[string]interface{}{"v": UnexportedStringer{n: 1}},
true,
"",
},
{
map[string]interface{}{"v": UnexportedStringer{n: 1}},
map[string]interface{}{"v": UnexportedStringer{n: 2}},
false,
"",
},
{
UnexportedBinaryer{n: 1},
UnexportedBinaryer{n: 1},
true,
"",
},
{
UnexportedBinaryer{n: 1},
UnexportedBinaryer{n: 2},
false,
"",
},
{
[]interface{}{UnexportedBinaryer{n: 1}},
[]interface{}{UnexportedBinaryer{n: 1}},
true,
"",
},
{
[]interface{}{UnexportedBinaryer{n: 1}},
[]interface{}{UnexportedBinaryer{n: 2}},
false,
"",
},
{
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
true,
"",
},
{
map[string]interface{}{"v": UnexportedBinaryer{n: 1}},
map[string]interface{}{"v": UnexportedBinaryer{n: 2}},
false,
"",
},
}

for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
one, err := Hash(tc.One, testFormat, &HashOptions{UnhashedStructFallback: true})
if tc.Err != "" {
if err == nil {
t.Fatal("expected error")
}

if !strings.Contains(err.Error(), tc.Err) {
t.Fatalf("expected error to contain %q, got: %s", tc.Err, err)
}

return
}
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}

two, err := Hash(tc.Two, testFormat, &HashOptions{UnhashedStructFallback: true})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}

// Zero is always wrong
if one == 0 {
t.Fatalf("zero hash: %#v", tc.One)
}

// Compare
if (one == two) != tc.Match {
t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two)
}
})
}
}