From 5933d4ad4bdd2149249463d79ee65bb0af3437a3 Mon Sep 17 00:00:00 2001 From: Adam Hasselbalch Hansen Date: Wed, 28 Aug 2019 11:29:08 +0200 Subject: [PATCH 1/7] Update github path in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28ce45a..522bb1f 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ sending data across the network, caching values locally (de-dup), and so on. Standard `go get`: ``` -$ go get github.com/mitchellh/hashstructure +$ go get github.com/adamhassel/hashstructure ``` ## Usage & Example -For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure). +For usage and examples see the [Godoc](http://godoc.org/github.com/adamhassel/hashstructure). A quick code example is shown below: From ecaffc76a69dcb5196c7140fec54b68e5def605c Mon Sep 17 00:00:00 2001 From: Adam Hasselbalch Hansen Date: Wed, 28 Aug 2019 12:01:04 +0200 Subject: [PATCH 2/7] don't continue --- hashstructure.go | 1 - 1 file changed, 1 deletion(-) diff --git a/hashstructure.go b/hashstructure.go index ff6c82c..5bfc483 100644 --- a/hashstructure.go +++ b/hashstructure.go @@ -272,7 +272,6 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { if w.stringer { if impl, ok := innerV.Interface().(fmt.Stringer); ok { innerV = reflect.ValueOf(impl.String()) - continue } } From 936a4fb409149919b880383548780d8a4a348af7 Mon Sep 17 00:00:00 2001 From: Lukas Rist Date: Tue, 18 Feb 2020 11:05:37 +0100 Subject: [PATCH 3/7] fix module name (#2) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 966582a..4023da3 100644 --- a/go.mod +++ b/go.mod @@ -1 +1 @@ -module github.com/mitchellh/hashstructure +module github.com/adamhassel/hashstructure From 3eff1aa57a990903ad087b0382772539c1148d7b Mon Sep 17 00:00:00 2001 From: Adam Hasselbalch Hansen Date: Wed, 19 Feb 2020 12:43:26 +0100 Subject: [PATCH 4/7] Correct doc links in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 522bb1f..b8029ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# hashstructure [![GoDoc](https://godoc.org/github.com/mitchellh/hashstructure?status.svg)](https://godoc.org/github.com/mitchellh/hashstructure) +# hashstructure [![GoDoc](https://godoc.org/github.com/adamhassel/hashstructure?status.svg)](https://godoc.org/github.com/adamhassel/hashstructure) hashstructure is a Go library for creating a unique hash value for arbitrary values in Go. From f002e50891e8fee93f19a13db9bf2ef64e146d9a Mon Sep 17 00:00:00 2001 From: Adam Hasselblach Hansen Date: Mon, 16 May 2022 15:20:09 +0200 Subject: [PATCH 5/7] Option to use MarshalBinary whenever possible --- hashstructure.go | 42 ++++++++++++++++++++++--------- hashstructure_test.go | 58 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/hashstructure.go b/hashstructure.go index 3dc0eb7..6a5dd5e 100644 --- a/hashstructure.go +++ b/hashstructure.go @@ -1,6 +1,7 @@ package hashstructure import ( + "encoding" "encoding/binary" "fmt" "hash" @@ -37,6 +38,11 @@ type HashOptions struct { // precedence (meaning that if the type doesn't implement fmt.Stringer, we // panic) UseStringer bool + + // UseBinary will use the encoding.BinaryMarshaler for any type that implements + // that interface. Common types are time.Time and url.URL. Note that if you explicitly set + // the 'string' tag on a field, that will take precedence over this option. + UseBinary bool } // Format specifies the hashing process used. Different formats typically @@ -124,6 +130,7 @@ func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) { ignorezerovalue: opts.IgnoreZeroValue, sets: opts.SlicesAsSets, stringer: opts.UseStringer, + binary: opts.UseBinary, } return w.visit(reflect.ValueOf(v), nil) } @@ -136,6 +143,7 @@ type walker struct { ignorezerovalue bool sets bool stringer bool + binary bool } type visitOpts struct { @@ -204,18 +212,6 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { 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 @@ -286,6 +282,11 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { return impl.Hash() } + // Use BinaryMarshaler whenever possible + if bm, ok := parent.(encoding.BinaryMarshaler); w.binary && ok { + return hashBinary(w.h, bm) + } + // If we can address this value, check if the pointer value // implements our interfaces and use that if so. if v.CanAddr() { @@ -298,6 +299,11 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { if impl, ok := parentptr.(Hashable); ok { return impl.Hash() } + + // Use BinaryMarshaler whenever possible + if bm, ok := parentptr.(encoding.BinaryMarshaler); w.binary && ok { + return hashBinary(w.h, bm) + } } t := v.Type() @@ -443,6 +449,18 @@ func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { return h.Sum64() } +// hashBinary will use the BinaryMarshaler implementation of types implementing that interface to generate a hash. Like time.Time or url.URL +func hashBinary(h hash.Hash64, bm encoding.BinaryMarshaler) (uint64, error) { + b, err := bm.MarshalBinary() + if err != nil { + return 0, err + } + + h.Reset() + err = binary.Write(h, binary.LittleEndian, b) + return h.Sum64(), err +} + func hashUpdateUnordered(a, b uint64) uint64 { return a ^ b } diff --git a/hashstructure_test.go b/hashstructure_test.go index 7b0034a..25d7b82 100644 --- a/hashstructure_test.go +++ b/hashstructure_test.go @@ -166,18 +166,46 @@ func TestHash_equal(t *testing.T) { now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock true, }, + { + struct { + Foo time.Time + }{ + Foo: now, // contains monotonic clock + }, + struct { + Foo time.Time + }{ + time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), + now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock + }, + true, + }, + { + struct { + Foo time.Time `hash:"string"` + }{ + Foo: now, // contains monotonic clock + }, + struct { + Foo time.Time `hash:"string"` + }{ + time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), + now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock + }, + false, + }, } for i, tc := range cases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { t.Logf("Hashing: %#v", tc.One) - one, err := Hash(tc.One, testFormat, nil) + one, err := Hash(tc.One, testFormat, &HashOptions{UseBinary: true}) t.Logf("Result: %d", one) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.One, err) } t.Logf("Hashing: %#v", tc.Two) - two, err := Hash(tc.Two, testFormat, nil) + two, err := Hash(tc.Two, testFormat, &HashOptions{UseBinary: true}) t.Logf("Result: %d", two) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.Two, err) @@ -187,7 +215,29 @@ func TestHash_equal(t *testing.T) { 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) + } + }) + t.Run(fmt.Sprintf("%d_UseStringer", i), func(t *testing.T) { + t.Logf("Hashing: %#v", tc.One) + one, err := Hash(tc.One, testFormat, &HashOptions{UseBinary: true}) + t.Logf("Result: %d", one) + if err != nil { + t.Fatalf("Failed to hash %#v: %s", tc.One, err) + } + t.Logf("Hashing: %#v", tc.Two) + two, err := Hash(tc.Two, testFormat, &HashOptions{UseBinary: true}) + t.Logf("Result: %d", two) + 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) @@ -270,11 +320,11 @@ func TestHash_equalIgnore(t *testing.T) { } for _, tc := range cases { - one, err := Hash(tc.One, testFormat, nil) + one, err := Hash(tc.One, testFormat, &HashOptions{UseBinary: true}) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.One, err) } - two, err := Hash(tc.Two, testFormat, nil) + two, err := Hash(tc.Two, testFormat, &HashOptions{UseBinary: true}) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.Two, err) } From 7193e11cdb47eadbdb4b8753526e1d7ee4223633 Mon Sep 17 00:00:00 2001 From: Adam Hasselblach Hansen Date: Tue, 17 May 2022 15:15:10 +0200 Subject: [PATCH 6/7] Fix UseStringer with UseBinary option --- hashstructure.go | 19 +++++++-- hashstructure_test.go | 98 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/hashstructure.go b/hashstructure.go index 6a5dd5e..26f0f45 100644 --- a/hashstructure.go +++ b/hashstructure.go @@ -40,8 +40,10 @@ type HashOptions struct { UseStringer bool // UseBinary will use the encoding.BinaryMarshaler for any type that implements - // that interface. Common types are time.Time and url.URL. Note that if you explicitly set - // the 'string' tag on a field, that will take precedence over this option. + // that interface. Common types are time.Time and url.URL. Note that if you + // explicitly set the 'string' tag on a field, that will take precedence over + // this option. If both UseStringer and UseBinary are set, UseBinary takes + // precedence for types that satisfy both options. UseBinary bool } @@ -334,8 +336,10 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { } } - // if string is set, use the string value - if tag == "string" || w.stringer { + // if string is set, use the string value, if not using stringers + //fmt.Println(canBinary(innerV.Interface())) + if tag == "string" || (w.stringer && !w.binary && !canBinary(innerV.Interface())) { + //fmt.Printf("string!!") if impl, ok := innerV.Interface().(fmt.Stringer); ok { innerV = reflect.ValueOf(impl.String()) } else if tag == "string" { @@ -449,6 +453,12 @@ func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { return h.Sum64() } +// canBinary returns a true value if i implements encoding.Binary +func canBinary(i interface{}) bool { + _, ok := i.(encoding.BinaryMarshaler) + return ok +} + // hashBinary will use the BinaryMarshaler implementation of types implementing that interface to generate a hash. Like time.Time or url.URL func hashBinary(h hash.Hash64, bm encoding.BinaryMarshaler) (uint64, error) { b, err := bm.MarshalBinary() @@ -458,6 +468,7 @@ func hashBinary(h hash.Hash64, bm encoding.BinaryMarshaler) (uint64, error) { h.Reset() err = binary.Write(h, binary.LittleEndian, b) + fmt.Printf("bin hashed! %+v %d\n", bm, h.Sum64()) return h.Sum64(), err } diff --git a/hashstructure_test.go b/hashstructure_test.go index 25d7b82..eda938d 100644 --- a/hashstructure_test.go +++ b/hashstructure_test.go @@ -192,7 +192,7 @@ func TestHash_equal(t *testing.T) { time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock }, - false, + false, // False, since we need to test that setting the `string` tag disables binary hashing }, } @@ -222,13 +222,13 @@ func TestHash_equal(t *testing.T) { }) t.Run(fmt.Sprintf("%d_UseStringer", i), func(t *testing.T) { t.Logf("Hashing: %#v", tc.One) - one, err := Hash(tc.One, testFormat, &HashOptions{UseBinary: true}) + one, err := Hash(tc.One, testFormat, &HashOptions{UseStringer: true, UseBinary: true}) t.Logf("Result: %d", one) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.One, err) } t.Logf("Hashing: %#v", tc.Two) - two, err := Hash(tc.Two, testFormat, &HashOptions{UseBinary: true}) + two, err := Hash(tc.Two, testFormat, &HashOptions{UseStringer: true, UseBinary: true}) t.Logf("Result: %d", two) if err != nil { t.Fatalf("Failed to hash %#v: %s", tc.Two, err) @@ -725,6 +725,98 @@ func TestHash_hashable(t *testing.T) { }) } } +func TestHash_binary(t *testing.T) { + now := time.Now() + type TestTime struct { + Name string + T time.Time + } + type TestTimeTag struct { + Name string + T time.Time `hash:"string"` + } + cases := []struct { + One, Two interface{} + Match bool + S bool // set to true if test should use "UseStringer" + B bool // set to true if test should use "UseBinary" + Err string + }{ + { + TestTime{"One", now}, + TestTime{"Two", now.Add(time.Second)}, + false, + false, + true, + "", + }, + { + TestTime{Name: "monotonic clock binary", T: now}, + TestTime{Name: "monotonic clock binary", T: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), + now.Minute(), now.Second(), now.Nanosecond(), now.Location()), + }, + true, + false, + true, + "", + }, + { + TestTimeTag{Name: "monotonic clock binary string tag", T: now}, + TestTimeTag{Name: "monotonic clock binary string tag", T: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), + now.Minute(), now.Second(), now.Nanosecond(), now.Location()), + }, + false, // string tag overrides the binary option + false, + true, + "", + }, + { + TestTime{Name: "monotonic clock binary stringer option", T: now}, + TestTime{Name: "monotonic clock binary stringer option", T: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), + now.Minute(), now.Second(), now.Nanosecond(), now.Location()), + }, + true, // UseStringer option should NOT override the binary option + false, + true, + "", + }, + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + one, err := Hash(tc.One, testFormat, &HashOptions{UseBinary: tc.B, UseStringer: tc.S}) + 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{UseBinary: tc.B, UseStringer: tc.S}) + 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: %t\n%+v\n%d\n%+v\n%d", tc.Match, tc.One, one, tc.Two, two) + } + }) + } +} type testIncludable struct { Value string From e0363ffea9fef4218072822fbc55fb75ddb69e8f Mon Sep 17 00:00:00 2001 From: Adam Hasselblach Hansen Date: Tue, 17 May 2022 15:19:26 +0200 Subject: [PATCH 7/7] Cleanup --- hashstructure.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/hashstructure.go b/hashstructure.go index 26f0f45..25df480 100644 --- a/hashstructure.go +++ b/hashstructure.go @@ -337,9 +337,7 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { } // if string is set, use the string value, if not using stringers - //fmt.Println(canBinary(innerV.Interface())) if tag == "string" || (w.stringer && !w.binary && !canBinary(innerV.Interface())) { - //fmt.Printf("string!!") if impl, ok := innerV.Interface().(fmt.Stringer); ok { innerV = reflect.ValueOf(impl.String()) } else if tag == "string" { @@ -468,7 +466,6 @@ func hashBinary(h hash.Hash64, bm encoding.BinaryMarshaler) (uint64, error) { h.Reset() err = binary.Write(h, binary.LittleEndian, b) - fmt.Printf("bin hashed! %+v %d\n", bm, h.Sum64()) return h.Sum64(), err }