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

More details

Full run details