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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -30,7 +30,9 @@ sending data across the network, caching values locally (de-dup), and so on.
Standard `go get`:

```
$ go get github.com/mitchellh/hashstructure/v2

$ go get github.com/adamhassel/hashstructure/v2

```

**Note on v2:** It is highly recommended you use the "v2" release since this
Expand All @@ -44,7 +46,7 @@ When using v2+, you can still generate weaker v1 hashes by using the

## 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:

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/mitchellh/hashstructure/v2
module github.com/adamhassel/hashstructure/v2

go 1.14
54 changes: 40 additions & 14 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,13 @@ 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. If both UseStringer and UseBinary are set, UseBinary takes
// precedence for types that satisfy both options.
UseBinary bool
}

// Format specifies the hashing process used. Different formats typically
Expand Down Expand Up @@ -124,6 +132,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)
}
Expand All @@ -136,6 +145,7 @@ type walker struct {
ignorezerovalue bool
sets bool
stringer bool
binary bool
}

type visitOpts struct {
Expand Down Expand Up @@ -204,18 +214,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
Expand Down Expand Up @@ -286,6 +284,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() {
Expand All @@ -298,6 +301,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()
Expand Down Expand Up @@ -328,8 +336,8 @@ 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
if tag == "string" || (w.stringer && !w.binary && !canBinary(innerV.Interface())) {
if impl, ok := innerV.Interface().(fmt.Stringer); ok {
innerV = reflect.ValueOf(impl.String())
} else if tag == "string" {
Expand Down Expand Up @@ -443,6 +451,24 @@ 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()
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
}
Expand Down
150 changes: 146 additions & 4 deletions hashstructure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, // False, since we need to test that setting the `string` tag disables binary hashing
},
}

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)
Expand All @@ -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{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{UseStringer: true, 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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -675,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
Expand Down