New Upstream Snapshot - golang-github-mitchellh-hashstructure
Ready changes
Summary
Merged new upstream version: 2.0.2+ds (was: 1.1.0).
Resulting package
Built on 2022-08-26T17:59 (took 3m57s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-snapshots golang-github-mitchellh-hashstructure-dev
Lintian Result
Diff
diff --git a/README.md b/README.md
index feb0c24..21f36be 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,18 @@ 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/mitchellh/hashstructure/v2
```
+**Note on v2:** It is highly recommended you use the "v2" release since this
+fixes some significant hash collisions issues from v1. In practice, we used
+v1 for many years in real projects at HashiCorp and never had issues, but it
+is highly dependent on the shape of the data you're hashing and how you use
+those hashes.
+
+When using v2+, you can still generate weaker v1 hashes by using the
+`FormatV1` format when calling `Hash`.
+
## Usage & Example
For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure).
@@ -56,7 +65,7 @@ v := ComplexStruct{
},
}
-hash, err := hashstructure.Hash(v, nil)
+hash, err := hashstructure.Hash(v, hashstructure.FormatV2, nil)
if err != nil {
panic(err)
}
diff --git a/debian/changelog b/debian/changelog
index d5ec971..a1a3fd7 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-mitchellh-hashstructure (2.0.2+ds-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Fri, 26 Aug 2022 17:56:30 -0000
+
golang-github-mitchellh-hashstructure (1.1.0-1) unstable; urgency=medium
[ Debian Janitor (Jelmer Vernooij) ]
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..44b8951
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,22 @@
+package hashstructure
+
+import (
+ "fmt"
+)
+
+// 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)
+}
+
+// ErrFormat is returned when an invalid format is given to the Hash function.
+type ErrFormat struct{}
+
+func (*ErrFormat) Error() string {
+ return "format must be one of the defined Format values in the hashstructure library"
+}
diff --git a/go.mod b/go.mod
index 981e501..7f7736c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module github.com/mitchellh/hashstructure
+module github.com/mitchellh/hashstructure/v2
go 1.14
diff --git a/hashstructure.go b/hashstructure.go
index 89dd4d3..3dc0eb7 100644
--- a/hashstructure.go
+++ b/hashstructure.go
@@ -9,16 +9,6 @@ import (
"time"
)
-// 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
@@ -41,14 +31,33 @@ type HashOptions struct {
// Default is false (in which case the tag is used instead)
SlicesAsSets bool
- // UseStringer will attempt to use fmt.Stringer aways. If the struct
+ // 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
- // precedense (meaning that if the type doesn't implement fmt.Stringer, we
+ // 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
@@ -56,6 +65,11 @@ type HashOptions struct {
// 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
@@ -81,7 +95,12 @@ type HashOptions struct {
// * "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) {
+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{}
@@ -98,6 +117,7 @@ func Hash(v interface{}, opts *HashOptions) (uint64, error) {
// Create our walker and walk the structure
w := &walker{
+ format: format,
h: opts.Hasher,
tag: opts.TagName,
zeronil: opts.ZeroNil,
@@ -109,6 +129,7 @@ func Hash(v interface{}, opts *HashOptions) (uint64, error) {
}
type walker struct {
+ format Format
h hash.Hash64
tag string
zeronil bool
@@ -247,6 +268,11 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
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:
@@ -283,7 +309,6 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
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 != "" {
@@ -298,8 +323,7 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
}
if w.ignorezerovalue {
- zeroVal := reflect.Zero(reflect.TypeOf(innerV.Interface())).Interface()
- if innerV.Interface() == zeroVal {
+ if innerV.IsZero() {
continue
}
}
@@ -350,6 +374,11 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
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
@@ -377,6 +406,11 @@ func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) {
}
}
+ if set && w.format != FormatV1 {
+ // Important: read the docs for hashFinishUnordered
+ h = hashFinishUnordered(w.h, h)
+ }
+
return h, nil
case reflect.String:
@@ -413,6 +447,32 @@ 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
diff --git a/hashstructure_examples_test.go b/hashstructure_examples_test.go
index 11d6efd..b598df1 100644
--- a/hashstructure_examples_test.go
+++ b/hashstructure_examples_test.go
@@ -21,7 +21,34 @@ func ExampleHash() {
},
}
- hash, err := Hash(v, nil)
+ hash, err := Hash(v, FormatV2, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%d", hash)
+ // Output:
+ // 1839806922502695369
+}
+
+func ExampleHash_v1() {
+ type ComplexStruct struct {
+ Name string
+ Age uint
+ Metadata map[string]interface{}
+ }
+
+ v := ComplexStruct{
+ Name: "mitchellh",
+ Age: 64,
+ Metadata: map[string]interface{}{
+ "car": true,
+ "location": "California",
+ "siblings": []string{"Bob", "John"},
+ },
+ }
+
+ hash, err := Hash(v, FormatV1, nil)
if err != nil {
panic(err)
}
diff --git a/hashstructure_test.go b/hashstructure_test.go
index 37caedd..7b0034a 100644
--- a/hashstructure_test.go
+++ b/hashstructure_test.go
@@ -7,6 +7,8 @@ import (
"time"
)
+var testFormat = FormatV2
+
func TestHash_identity(t *testing.T) {
cases := []interface{}{
nil,
@@ -40,7 +42,7 @@ func TestHash_identity(t *testing.T) {
// in the runtime in terms of ordering.
valuelist := make([]uint64, 100)
for i := range valuelist {
- v, err := Hash(tc, nil)
+ v, err := Hash(tc, testFormat, nil)
if err != nil {
t.Fatalf("Error: %s\n\n%#v", err, tc)
}
@@ -100,7 +102,7 @@ func TestHash_equal(t *testing.T) {
{
struct{ Lname, Fname string }{"foo", "bar"},
struct{ Fname, Lname string }{"bar", "foo"},
- true,
+ false,
},
{
@@ -169,13 +171,13 @@ func TestHash_equal(t *testing.T) {
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, nil)
+ one, err := Hash(tc.One, testFormat, nil)
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, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
t.Logf("Result: %d", two)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
@@ -268,11 +270,11 @@ func TestHash_equalIgnore(t *testing.T) {
}
for _, tc := range cases {
- one, err := Hash(tc.One, nil)
+ one, err := Hash(tc.One, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
@@ -324,7 +326,7 @@ func TestHash_stringTagError(t *testing.T) {
}
for _, tc := range cases {
- _, err := Hash(tc.Test, nil)
+ _, err := Hash(tc.Test, testFormat, nil)
if err != nil {
if ens, ok := err.(*ErrNotStringer); ok {
if ens.Field != tc.Field {
@@ -397,11 +399,11 @@ func TestHash_equalNil(t *testing.T) {
}
for _, tc := range cases {
- one, err := Hash(tc.One, &HashOptions{ZeroNil: tc.ZeroNil})
+ one, err := Hash(tc.One, testFormat, &HashOptions{ZeroNil: tc.ZeroNil})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, &HashOptions{ZeroNil: tc.ZeroNil})
+ two, err := Hash(tc.Two, testFormat, &HashOptions{ZeroNil: tc.ZeroNil})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
@@ -442,11 +444,11 @@ func TestHash_equalSet(t *testing.T) {
}
for _, tc := range cases {
- one, err := Hash(tc.One, nil)
+ one, err := Hash(tc.One, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
@@ -488,11 +490,11 @@ func TestHash_includable(t *testing.T) {
}
for _, tc := range cases {
- one, err := Hash(tc.One, nil)
+ one, err := Hash(tc.One, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
@@ -523,6 +525,7 @@ func TestHash_ignoreZeroValue(t *testing.T) {
structA := struct {
Foo string
Bar string
+ Map map[string]int
}{
Foo: "foo",
Bar: "bar",
@@ -531,17 +534,18 @@ func TestHash_ignoreZeroValue(t *testing.T) {
Foo string
Bar string
Baz string
+ Map map[string]int
}{
Foo: "foo",
Bar: "bar",
}
for _, tc := range cases {
- hashA, err := Hash(structA, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue})
+ hashA, err := Hash(structA, testFormat, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", structA, err)
}
- hashB, err := Hash(structB, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue})
+ hashB, err := Hash(structB, testFormat, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue})
if err != nil {
t.Fatalf("Failed to hash %#v: %s", structB, err)
}
@@ -576,11 +580,11 @@ func TestHash_includableMap(t *testing.T) {
}
for _, tc := range cases {
- one, err := Hash(tc.One, nil)
+ one, err := Hash(tc.One, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
@@ -638,7 +642,7 @@ func TestHash_hashable(t *testing.T) {
for i, tc := range cases {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
- one, err := Hash(tc.One, nil)
+ one, err := Hash(tc.One, testFormat, nil)
if tc.Err != "" {
if err == nil {
t.Fatal("expected error")
@@ -654,7 +658,7 @@ func TestHash_hashable(t *testing.T) {
t.Fatalf("Failed to hash %#v: %s", tc.One, err)
}
- two, err := Hash(tc.Two, nil)
+ two, err := Hash(tc.Two, testFormat, nil)
if err != nil {
t.Fatalf("Failed to hash %#v: %s", tc.Two, err)
}
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/gocode/src/github.com/mitchellh/hashstructure/errors.go
No differences were encountered in the control files