├── .github └── workflows │ └── test.yml ├── LICENSE ├── README.md ├── errors.go ├── go.mod ├── hashstructure.go ├── hashstructure_examples_test.go ├── hashstructure_test.go └── include.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.15.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: go test ./... 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mitchell Hashimoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hashstructure [![GoDoc](https://godoc.org/github.com/mitchellh/hashstructure?status.svg)](https://godoc.org/github.com/mitchellh/hashstructure) 2 | 3 | hashstructure is a Go library for creating a unique hash value 4 | for arbitrary values in Go. 5 | 6 | This can be used to key values in a hash (for use in a map, set, etc.) 7 | that are complex. The most common use case is comparing two values without 8 | sending data across the network, caching values locally (de-dup), and so on. 9 | 10 | ## Features 11 | 12 | * Hash any arbitrary Go value, including complex types. 13 | 14 | * Tag a struct field to ignore it and not affect the hash value. 15 | 16 | * Tag a slice type struct field to treat it as a set where ordering 17 | doesn't affect the hash code but the field itself is still taken into 18 | account to create the hash value. 19 | 20 | * Optionally, specify a custom hash function to optimize for speed, collision 21 | avoidance for your data set, etc. 22 | 23 | * Optionally, hash the output of `.String()` on structs that implement fmt.Stringer, 24 | allowing effective hashing of time.Time 25 | 26 | * Optionally, override the hashing process by implementing `Hashable`. 27 | 28 | ## Installation 29 | 30 | Standard `go get`: 31 | 32 | ``` 33 | $ go get github.com/mitchellh/hashstructure/v2 34 | ``` 35 | 36 | **Note on v2:** It is highly recommended you use the "v2" release since this 37 | fixes some significant hash collisions issues from v1. In practice, we used 38 | v1 for many years in real projects at HashiCorp and never had issues, but it 39 | is highly dependent on the shape of the data you're hashing and how you use 40 | those hashes. 41 | 42 | When using v2+, you can still generate weaker v1 hashes by using the 43 | `FormatV1` format when calling `Hash`. 44 | 45 | ## Usage & Example 46 | 47 | For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/hashstructure). 48 | 49 | A quick code example is shown below: 50 | 51 | ```go 52 | type ComplexStruct struct { 53 | Name string 54 | Age uint 55 | Metadata map[string]interface{} 56 | } 57 | 58 | v := ComplexStruct{ 59 | Name: "mitchellh", 60 | Age: 64, 61 | Metadata: map[string]interface{}{ 62 | "car": true, 63 | "location": "California", 64 | "siblings": []string{"Bob", "John"}, 65 | }, 66 | } 67 | 68 | hash, err := hashstructure.Hash(v, hashstructure.FormatV2, nil) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | fmt.Printf("%d", hash) 74 | // Output: 75 | // 2307517237273902113 76 | ``` 77 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ErrNotStringer is returned when there's an error with hash:"string" 8 | type ErrNotStringer struct { 9 | Field string 10 | } 11 | 12 | // Error implements error for ErrNotStringer 13 | func (ens *ErrNotStringer) Error() string { 14 | return fmt.Sprintf("hashstructure: %s has hash:\"string\" set, but does not implement fmt.Stringer", ens.Field) 15 | } 16 | 17 | // ErrFormat is returned when an invalid format is given to the Hash function. 18 | type ErrFormat struct{} 19 | 20 | func (*ErrFormat) Error() string { 21 | return "format must be one of the defined Format values in the hashstructure library" 22 | } 23 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mitchellh/hashstructure/v2 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /hashstructure.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "hash" 7 | "hash/fnv" 8 | "reflect" 9 | "time" 10 | ) 11 | 12 | // HashOptions are options that are available for hashing. 13 | type HashOptions struct { 14 | // Hasher is the hash function to use. If this isn't set, it will 15 | // default to FNV. 16 | Hasher hash.Hash64 17 | 18 | // TagName is the struct tag to look at when hashing the structure. 19 | // By default this is "hash". 20 | TagName string 21 | 22 | // ZeroNil is flag determining if nil pointer should be treated equal 23 | // to a zero value of pointed type. By default this is false. 24 | ZeroNil bool 25 | 26 | // IgnoreZeroValue is determining if zero value fields should be 27 | // ignored for hash calculation. 28 | IgnoreZeroValue bool 29 | 30 | // SlicesAsSets assumes that a `set` tag is always present for slices. 31 | // Default is false (in which case the tag is used instead) 32 | SlicesAsSets bool 33 | 34 | // UseStringer will attempt to use fmt.Stringer always. If the struct 35 | // doesn't implement fmt.Stringer, it'll fall back to trying usual tricks. 36 | // If this is true, and the "string" tag is also set, the tag takes 37 | // precedence (meaning that if the type doesn't implement fmt.Stringer, we 38 | // panic) 39 | UseStringer bool 40 | } 41 | 42 | // Format specifies the hashing process used. Different formats typically 43 | // generate different hashes for the same value and have different properties. 44 | type Format uint 45 | 46 | const ( 47 | // To disallow the zero value 48 | formatInvalid Format = iota 49 | 50 | // FormatV1 is the format used in v1.x of this library. This has the 51 | // downsides noted in issue #18 but allows simultaneous v1/v2 usage. 52 | FormatV1 53 | 54 | // FormatV2 is the current recommended format and fixes the issues 55 | // noted in FormatV1. 56 | FormatV2 57 | 58 | formatMax // so we can easily find the end 59 | ) 60 | 61 | // Hash returns the hash value of an arbitrary value. 62 | // 63 | // If opts is nil, then default options will be used. See HashOptions 64 | // for the default values. The same *HashOptions value cannot be used 65 | // concurrently. None of the values within a *HashOptions struct are 66 | // safe to read/write while hashing is being done. 67 | // 68 | // The "format" is required and must be one of the format values defined 69 | // by this library. You should probably just use "FormatV2". This allows 70 | // generated hashes uses alternate logic to maintain compatibility with 71 | // older versions. 72 | // 73 | // Notes on the value: 74 | // 75 | // * Unexported fields on structs are ignored and do not affect the 76 | // hash value. 77 | // 78 | // * Adding an exported field to a struct with the zero value will change 79 | // the hash value. 80 | // 81 | // For structs, the hashing can be controlled using tags. For example: 82 | // 83 | // struct { 84 | // Name string 85 | // UUID string `hash:"ignore"` 86 | // } 87 | // 88 | // The available tag values are: 89 | // 90 | // * "ignore" or "-" - The field will be ignored and not affect the hash code. 91 | // 92 | // * "set" - The field will be treated as a set, where ordering doesn't 93 | // affect the hash code. This only works for slices. 94 | // 95 | // * "string" - The field will be hashed as a string, only works when the 96 | // field implements fmt.Stringer 97 | // 98 | func Hash(v interface{}, format Format, opts *HashOptions) (uint64, error) { 99 | // Validate our format 100 | if format <= formatInvalid || format >= formatMax { 101 | return 0, &ErrFormat{} 102 | } 103 | 104 | // Create default options 105 | if opts == nil { 106 | opts = &HashOptions{} 107 | } 108 | if opts.Hasher == nil { 109 | opts.Hasher = fnv.New64() 110 | } 111 | if opts.TagName == "" { 112 | opts.TagName = "hash" 113 | } 114 | 115 | // Reset the hash 116 | opts.Hasher.Reset() 117 | 118 | // Create our walker and walk the structure 119 | w := &walker{ 120 | format: format, 121 | h: opts.Hasher, 122 | tag: opts.TagName, 123 | zeronil: opts.ZeroNil, 124 | ignorezerovalue: opts.IgnoreZeroValue, 125 | sets: opts.SlicesAsSets, 126 | stringer: opts.UseStringer, 127 | } 128 | return w.visit(reflect.ValueOf(v), nil) 129 | } 130 | 131 | type walker struct { 132 | format Format 133 | h hash.Hash64 134 | tag string 135 | zeronil bool 136 | ignorezerovalue bool 137 | sets bool 138 | stringer bool 139 | } 140 | 141 | type visitOpts struct { 142 | // Flags are a bitmask of flags to affect behavior of this visit 143 | Flags visitFlag 144 | 145 | // Information about the struct containing this field 146 | Struct interface{} 147 | StructField string 148 | } 149 | 150 | var timeType = reflect.TypeOf(time.Time{}) 151 | 152 | func (w *walker) visit(v reflect.Value, opts *visitOpts) (uint64, error) { 153 | t := reflect.TypeOf(0) 154 | 155 | // Loop since these can be wrapped in multiple layers of pointers 156 | // and interfaces. 157 | for { 158 | // If we have an interface, dereference it. We have to do this up 159 | // here because it might be a nil in there and the check below must 160 | // catch that. 161 | if v.Kind() == reflect.Interface { 162 | v = v.Elem() 163 | continue 164 | } 165 | 166 | if v.Kind() == reflect.Ptr { 167 | if w.zeronil { 168 | t = v.Type().Elem() 169 | } 170 | v = reflect.Indirect(v) 171 | continue 172 | } 173 | 174 | break 175 | } 176 | 177 | // If it is nil, treat it like a zero. 178 | if !v.IsValid() { 179 | v = reflect.Zero(t) 180 | } 181 | 182 | // Binary writing can use raw ints, we have to convert to 183 | // a sized-int, we'll choose the largest... 184 | switch v.Kind() { 185 | case reflect.Int: 186 | v = reflect.ValueOf(int64(v.Int())) 187 | case reflect.Uint: 188 | v = reflect.ValueOf(uint64(v.Uint())) 189 | case reflect.Bool: 190 | var tmp int8 191 | if v.Bool() { 192 | tmp = 1 193 | } 194 | v = reflect.ValueOf(tmp) 195 | } 196 | 197 | k := v.Kind() 198 | 199 | // We can shortcut numeric values by directly binary writing them 200 | if k >= reflect.Int && k <= reflect.Complex64 { 201 | // A direct hash calculation 202 | w.h.Reset() 203 | err := binary.Write(w.h, binary.LittleEndian, v.Interface()) 204 | return w.h.Sum64(), err 205 | } 206 | 207 | switch v.Type() { 208 | case timeType: 209 | w.h.Reset() 210 | b, err := v.Interface().(time.Time).MarshalBinary() 211 | if err != nil { 212 | return 0, err 213 | } 214 | 215 | err = binary.Write(w.h, binary.LittleEndian, b) 216 | return w.h.Sum64(), err 217 | } 218 | 219 | switch k { 220 | case reflect.Array: 221 | var h uint64 222 | l := v.Len() 223 | for i := 0; i < l; i++ { 224 | current, err := w.visit(v.Index(i), nil) 225 | if err != nil { 226 | return 0, err 227 | } 228 | 229 | h = hashUpdateOrdered(w.h, h, current) 230 | } 231 | 232 | return h, nil 233 | 234 | case reflect.Map: 235 | var includeMap IncludableMap 236 | if opts != nil && opts.Struct != nil { 237 | if v, ok := opts.Struct.(IncludableMap); ok { 238 | includeMap = v 239 | } 240 | } 241 | 242 | // Build the hash for the map. We do this by XOR-ing all the key 243 | // and value hashes. This makes it deterministic despite ordering. 244 | var h uint64 245 | for _, k := range v.MapKeys() { 246 | v := v.MapIndex(k) 247 | if includeMap != nil { 248 | incl, err := includeMap.HashIncludeMap( 249 | opts.StructField, k.Interface(), v.Interface()) 250 | if err != nil { 251 | return 0, err 252 | } 253 | if !incl { 254 | continue 255 | } 256 | } 257 | 258 | kh, err := w.visit(k, nil) 259 | if err != nil { 260 | return 0, err 261 | } 262 | vh, err := w.visit(v, nil) 263 | if err != nil { 264 | return 0, err 265 | } 266 | 267 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 268 | h = hashUpdateUnordered(h, fieldHash) 269 | } 270 | 271 | if w.format != FormatV1 { 272 | // Important: read the docs for hashFinishUnordered 273 | h = hashFinishUnordered(w.h, h) 274 | } 275 | 276 | return h, nil 277 | 278 | case reflect.Struct: 279 | parent := v.Interface() 280 | var include Includable 281 | if impl, ok := parent.(Includable); ok { 282 | include = impl 283 | } 284 | 285 | if impl, ok := parent.(Hashable); ok { 286 | return impl.Hash() 287 | } 288 | 289 | // If we can address this value, check if the pointer value 290 | // implements our interfaces and use that if so. 291 | if v.CanAddr() { 292 | vptr := v.Addr() 293 | parentptr := vptr.Interface() 294 | if impl, ok := parentptr.(Includable); ok { 295 | include = impl 296 | } 297 | 298 | if impl, ok := parentptr.(Hashable); ok { 299 | return impl.Hash() 300 | } 301 | } 302 | 303 | t := v.Type() 304 | h, err := w.visit(reflect.ValueOf(t.Name()), nil) 305 | if err != nil { 306 | return 0, err 307 | } 308 | 309 | l := v.NumField() 310 | for i := 0; i < l; i++ { 311 | if innerV := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { 312 | var f visitFlag 313 | fieldType := t.Field(i) 314 | if fieldType.PkgPath != "" { 315 | // Unexported 316 | continue 317 | } 318 | 319 | tag := fieldType.Tag.Get(w.tag) 320 | if tag == "ignore" || tag == "-" { 321 | // Ignore this field 322 | continue 323 | } 324 | 325 | if w.ignorezerovalue { 326 | if innerV.IsZero() { 327 | continue 328 | } 329 | } 330 | 331 | // if string is set, use the string value 332 | if tag == "string" || w.stringer { 333 | if impl, ok := innerV.Interface().(fmt.Stringer); ok { 334 | innerV = reflect.ValueOf(impl.String()) 335 | } else if tag == "string" { 336 | // We only show this error if the tag explicitly 337 | // requests a stringer. 338 | return 0, &ErrNotStringer{ 339 | Field: v.Type().Field(i).Name, 340 | } 341 | } 342 | } 343 | 344 | // Check if we implement includable and check it 345 | if include != nil { 346 | incl, err := include.HashInclude(fieldType.Name, innerV) 347 | if err != nil { 348 | return 0, err 349 | } 350 | if !incl { 351 | continue 352 | } 353 | } 354 | 355 | switch tag { 356 | case "set": 357 | f |= visitFlagSet 358 | } 359 | 360 | kh, err := w.visit(reflect.ValueOf(fieldType.Name), nil) 361 | if err != nil { 362 | return 0, err 363 | } 364 | 365 | vh, err := w.visit(innerV, &visitOpts{ 366 | Flags: f, 367 | Struct: parent, 368 | StructField: fieldType.Name, 369 | }) 370 | if err != nil { 371 | return 0, err 372 | } 373 | 374 | fieldHash := hashUpdateOrdered(w.h, kh, vh) 375 | h = hashUpdateUnordered(h, fieldHash) 376 | } 377 | 378 | if w.format != FormatV1 { 379 | // Important: read the docs for hashFinishUnordered 380 | h = hashFinishUnordered(w.h, h) 381 | } 382 | } 383 | 384 | return h, nil 385 | 386 | case reflect.Slice: 387 | // We have two behaviors here. If it isn't a set, then we just 388 | // visit all the elements. If it is a set, then we do a deterministic 389 | // hash code. 390 | var h uint64 391 | var set bool 392 | if opts != nil { 393 | set = (opts.Flags & visitFlagSet) != 0 394 | } 395 | l := v.Len() 396 | for i := 0; i < l; i++ { 397 | current, err := w.visit(v.Index(i), nil) 398 | if err != nil { 399 | return 0, err 400 | } 401 | 402 | if set || w.sets { 403 | h = hashUpdateUnordered(h, current) 404 | } else { 405 | h = hashUpdateOrdered(w.h, h, current) 406 | } 407 | } 408 | 409 | if set && w.format != FormatV1 { 410 | // Important: read the docs for hashFinishUnordered 411 | h = hashFinishUnordered(w.h, h) 412 | } 413 | 414 | return h, nil 415 | 416 | case reflect.String: 417 | // Directly hash 418 | w.h.Reset() 419 | _, err := w.h.Write([]byte(v.String())) 420 | return w.h.Sum64(), err 421 | 422 | default: 423 | return 0, fmt.Errorf("unknown kind to hash: %s", k) 424 | } 425 | 426 | } 427 | 428 | func hashUpdateOrdered(h hash.Hash64, a, b uint64) uint64 { 429 | // For ordered updates, use a real hash function 430 | h.Reset() 431 | 432 | // We just panic if the binary writes fail because we are writing 433 | // an int64 which should never be fail-able. 434 | e1 := binary.Write(h, binary.LittleEndian, a) 435 | e2 := binary.Write(h, binary.LittleEndian, b) 436 | if e1 != nil { 437 | panic(e1) 438 | } 439 | if e2 != nil { 440 | panic(e2) 441 | } 442 | 443 | return h.Sum64() 444 | } 445 | 446 | func hashUpdateUnordered(a, b uint64) uint64 { 447 | return a ^ b 448 | } 449 | 450 | // After mixing a group of unique hashes with hashUpdateUnordered, it's always 451 | // necessary to call hashFinishUnordered. Why? Because hashUpdateUnordered 452 | // is a simple XOR, and calling hashUpdateUnordered on hashes produced by 453 | // hashUpdateUnordered can effectively cancel out a previous change to the hash 454 | // result if the same hash value appears later on. For example, consider: 455 | // 456 | // hashUpdateUnordered(hashUpdateUnordered("A", "B"), hashUpdateUnordered("A", "C")) = 457 | // H("A") ^ H("B")) ^ (H("A") ^ H("C")) = 458 | // (H("A") ^ H("A")) ^ (H("B") ^ H(C)) = 459 | // H(B) ^ H(C) = 460 | // hashUpdateUnordered(hashUpdateUnordered("Z", "B"), hashUpdateUnordered("Z", "C")) 461 | // 462 | // hashFinishUnordered "hardens" the result, so that encountering partially 463 | // overlapping input data later on in a different context won't cancel out. 464 | func hashFinishUnordered(h hash.Hash64, a uint64) uint64 { 465 | h.Reset() 466 | 467 | // We just panic if the writes fail 468 | e1 := binary.Write(h, binary.LittleEndian, a) 469 | if e1 != nil { 470 | panic(e1) 471 | } 472 | 473 | return h.Sum64() 474 | } 475 | 476 | // visitFlag is used as a bitmask for affecting visit behavior 477 | type visitFlag uint 478 | 479 | const ( 480 | visitFlagInvalid visitFlag = iota 481 | visitFlagSet = iota << 1 482 | ) 483 | -------------------------------------------------------------------------------- /hashstructure_examples_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ExampleHash() { 8 | type ComplexStruct struct { 9 | Name string 10 | Age uint 11 | Metadata map[string]interface{} 12 | } 13 | 14 | v := ComplexStruct{ 15 | Name: "mitchellh", 16 | Age: 64, 17 | Metadata: map[string]interface{}{ 18 | "car": true, 19 | "location": "California", 20 | "siblings": []string{"Bob", "John"}, 21 | }, 22 | } 23 | 24 | hash, err := Hash(v, FormatV2, nil) 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | fmt.Printf("%d", hash) 30 | // Output: 31 | // 1839806922502695369 32 | } 33 | 34 | func ExampleHash_v1() { 35 | type ComplexStruct struct { 36 | Name string 37 | Age uint 38 | Metadata map[string]interface{} 39 | } 40 | 41 | v := ComplexStruct{ 42 | Name: "mitchellh", 43 | Age: 64, 44 | Metadata: map[string]interface{}{ 45 | "car": true, 46 | "location": "California", 47 | "siblings": []string{"Bob", "John"}, 48 | }, 49 | } 50 | 51 | hash, err := Hash(v, FormatV1, nil) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | fmt.Printf("%d", hash) 57 | // Output: 58 | // 6691276962590150517 59 | } 60 | -------------------------------------------------------------------------------- /hashstructure_test.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | var testFormat = FormatV2 11 | 12 | func TestHash_identity(t *testing.T) { 13 | cases := []interface{}{ 14 | nil, 15 | "foo", 16 | 42, 17 | true, 18 | false, 19 | []string{"foo", "bar"}, 20 | []interface{}{1, nil, "foo"}, 21 | map[string]string{"foo": "bar"}, 22 | map[interface{}]string{"foo": "bar"}, 23 | map[interface{}]interface{}{"foo": "bar", "bar": 0}, 24 | struct { 25 | Foo string 26 | Bar []interface{} 27 | }{ 28 | Foo: "foo", 29 | Bar: []interface{}{nil, nil, nil}, 30 | }, 31 | &struct { 32 | Foo string 33 | Bar []interface{} 34 | }{ 35 | Foo: "foo", 36 | Bar: []interface{}{nil, nil, nil}, 37 | }, 38 | } 39 | 40 | for _, tc := range cases { 41 | // We run the test 100 times to try to tease out variability 42 | // in the runtime in terms of ordering. 43 | valuelist := make([]uint64, 100) 44 | for i := range valuelist { 45 | v, err := Hash(tc, testFormat, nil) 46 | if err != nil { 47 | t.Fatalf("Error: %s\n\n%#v", err, tc) 48 | } 49 | 50 | valuelist[i] = v 51 | } 52 | 53 | // Zero is always wrong 54 | if valuelist[0] == 0 { 55 | t.Fatalf("zero hash: %#v", tc) 56 | } 57 | 58 | // Make sure all the values match 59 | t.Logf("%#v: %d", tc, valuelist[0]) 60 | for i := 1; i < len(valuelist); i++ { 61 | if valuelist[i] != valuelist[0] { 62 | t.Fatalf("non-matching: %d, %d\n\n%#v", i, 0, tc) 63 | } 64 | } 65 | } 66 | } 67 | 68 | func TestHash_equal(t *testing.T) { 69 | type testFoo struct{ Name string } 70 | type testBar struct{ Name string } 71 | 72 | now := time.Now() 73 | 74 | cases := []struct { 75 | One, Two interface{} 76 | Match bool 77 | }{ 78 | { 79 | map[string]string{"foo": "bar"}, 80 | map[interface{}]string{"foo": "bar"}, 81 | true, 82 | }, 83 | 84 | { 85 | map[string]interface{}{"1": "1"}, 86 | map[string]interface{}{"1": "1", "2": "2"}, 87 | false, 88 | }, 89 | 90 | { 91 | struct{ Fname, Lname string }{"foo", "bar"}, 92 | struct{ Fname, Lname string }{"bar", "foo"}, 93 | false, 94 | }, 95 | 96 | { 97 | struct{ Lname, Fname string }{"foo", "bar"}, 98 | struct{ Fname, Lname string }{"foo", "bar"}, 99 | false, 100 | }, 101 | 102 | { 103 | struct{ Lname, Fname string }{"foo", "bar"}, 104 | struct{ Fname, Lname string }{"bar", "foo"}, 105 | false, 106 | }, 107 | 108 | { 109 | testFoo{"foo"}, 110 | testBar{"foo"}, 111 | false, 112 | }, 113 | 114 | { 115 | struct { 116 | Foo string 117 | unexported string 118 | }{ 119 | Foo: "bar", 120 | unexported: "baz", 121 | }, 122 | struct { 123 | Foo string 124 | unexported string 125 | }{ 126 | Foo: "bar", 127 | unexported: "bang", 128 | }, 129 | true, 130 | }, 131 | 132 | { 133 | struct { 134 | testFoo 135 | Foo string 136 | }{ 137 | Foo: "bar", 138 | testFoo: testFoo{Name: "baz"}, 139 | }, 140 | struct { 141 | testFoo 142 | Foo string 143 | }{ 144 | Foo: "bar", 145 | }, 146 | true, 147 | }, 148 | 149 | { 150 | struct { 151 | Foo string 152 | }{ 153 | Foo: "bar", 154 | }, 155 | struct { 156 | testFoo 157 | Foo string 158 | }{ 159 | Foo: "bar", 160 | }, 161 | true, 162 | }, 163 | { 164 | now, // contains monotonic clock 165 | time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 166 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), // does not contain monotonic clock 167 | true, 168 | }, 169 | } 170 | 171 | for i, tc := range cases { 172 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 173 | t.Logf("Hashing: %#v", tc.One) 174 | one, err := Hash(tc.One, testFormat, nil) 175 | t.Logf("Result: %d", one) 176 | if err != nil { 177 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 178 | } 179 | t.Logf("Hashing: %#v", tc.Two) 180 | two, err := Hash(tc.Two, testFormat, nil) 181 | t.Logf("Result: %d", two) 182 | if err != nil { 183 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 184 | } 185 | 186 | // Zero is always wrong 187 | if one == 0 { 188 | t.Fatalf("zero hash: %#v", tc.One) 189 | } 190 | 191 | // Compare 192 | if (one == two) != tc.Match { 193 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestHash_equalIgnore(t *testing.T) { 200 | type Test1 struct { 201 | Name string 202 | UUID string `hash:"ignore"` 203 | } 204 | 205 | type Test2 struct { 206 | Name string 207 | UUID string `hash:"-"` 208 | } 209 | 210 | type TestTime struct { 211 | Name string 212 | Time time.Time `hash:"string"` 213 | } 214 | 215 | type TestTime2 struct { 216 | Name string 217 | Time time.Time 218 | } 219 | 220 | now := time.Now() 221 | cases := []struct { 222 | One, Two interface{} 223 | Match bool 224 | }{ 225 | { 226 | Test1{Name: "foo", UUID: "foo"}, 227 | Test1{Name: "foo", UUID: "bar"}, 228 | true, 229 | }, 230 | 231 | { 232 | Test1{Name: "foo", UUID: "foo"}, 233 | Test1{Name: "foo", UUID: "foo"}, 234 | true, 235 | }, 236 | 237 | { 238 | Test2{Name: "foo", UUID: "foo"}, 239 | Test2{Name: "foo", UUID: "bar"}, 240 | true, 241 | }, 242 | 243 | { 244 | Test2{Name: "foo", UUID: "foo"}, 245 | Test2{Name: "foo", UUID: "foo"}, 246 | true, 247 | }, 248 | { 249 | TestTime{Name: "foo", Time: now}, 250 | TestTime{Name: "foo", Time: time.Time{}}, 251 | false, 252 | }, 253 | { 254 | TestTime{Name: "foo", Time: now}, 255 | TestTime{Name: "foo", Time: now}, 256 | true, 257 | }, 258 | { 259 | TestTime2{Name: "foo", Time: now}, 260 | TestTime2{Name: "foo", Time: time.Time{}}, 261 | false, 262 | }, 263 | { 264 | TestTime2{Name: "foo", Time: now}, 265 | TestTime2{Name: "foo", Time: time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 266 | now.Minute(), now.Second(), now.Nanosecond(), now.Location()), 267 | }, 268 | true, 269 | }, 270 | } 271 | 272 | for _, tc := range cases { 273 | one, err := Hash(tc.One, testFormat, nil) 274 | if err != nil { 275 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 276 | } 277 | two, err := Hash(tc.Two, testFormat, nil) 278 | if err != nil { 279 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 280 | } 281 | 282 | // Zero is always wrong 283 | if one == 0 { 284 | t.Fatalf("zero hash: %#v", tc.One) 285 | } 286 | 287 | // Compare 288 | if (one == two) != tc.Match { 289 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 290 | } 291 | } 292 | } 293 | 294 | func TestHash_stringTagError(t *testing.T) { 295 | type Test1 struct { 296 | Name string 297 | BrokenField string `hash:"string"` 298 | } 299 | 300 | type Test2 struct { 301 | Name string 302 | BustedField int `hash:"string"` 303 | } 304 | 305 | type Test3 struct { 306 | Name string 307 | Time time.Time `hash:"string"` 308 | } 309 | 310 | cases := []struct { 311 | Test interface{} 312 | Field string 313 | }{ 314 | { 315 | Test1{Name: "foo", BrokenField: "bar"}, 316 | "BrokenField", 317 | }, 318 | { 319 | Test2{Name: "foo", BustedField: 23}, 320 | "BustedField", 321 | }, 322 | { 323 | Test3{Name: "foo", Time: time.Now()}, 324 | "", 325 | }, 326 | } 327 | 328 | for _, tc := range cases { 329 | _, err := Hash(tc.Test, testFormat, nil) 330 | if err != nil { 331 | if ens, ok := err.(*ErrNotStringer); ok { 332 | if ens.Field != tc.Field { 333 | t.Fatalf("did not get expected field %#v: got %s wanted %s", tc.Test, ens.Field, tc.Field) 334 | } 335 | } else { 336 | t.Fatalf("unknown error %#v: got %s", tc, err) 337 | } 338 | } 339 | } 340 | } 341 | 342 | func TestHash_equalNil(t *testing.T) { 343 | type Test struct { 344 | Str *string 345 | Int *int 346 | Map map[string]string 347 | Slice []string 348 | } 349 | 350 | cases := []struct { 351 | One, Two interface{} 352 | ZeroNil bool 353 | Match bool 354 | }{ 355 | { 356 | Test{ 357 | Str: nil, 358 | Int: nil, 359 | Map: nil, 360 | Slice: nil, 361 | }, 362 | Test{ 363 | Str: new(string), 364 | Int: new(int), 365 | Map: make(map[string]string), 366 | Slice: make([]string, 0), 367 | }, 368 | true, 369 | true, 370 | }, 371 | { 372 | Test{ 373 | Str: nil, 374 | Int: nil, 375 | Map: nil, 376 | Slice: nil, 377 | }, 378 | Test{ 379 | Str: new(string), 380 | Int: new(int), 381 | Map: make(map[string]string), 382 | Slice: make([]string, 0), 383 | }, 384 | false, 385 | false, 386 | }, 387 | { 388 | nil, 389 | 0, 390 | true, 391 | true, 392 | }, 393 | { 394 | nil, 395 | 0, 396 | false, 397 | true, 398 | }, 399 | } 400 | 401 | for _, tc := range cases { 402 | one, err := Hash(tc.One, testFormat, &HashOptions{ZeroNil: tc.ZeroNil}) 403 | if err != nil { 404 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 405 | } 406 | two, err := Hash(tc.Two, testFormat, &HashOptions{ZeroNil: tc.ZeroNil}) 407 | if err != nil { 408 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 409 | } 410 | 411 | // Zero is always wrong 412 | if one == 0 { 413 | t.Fatalf("zero hash: %#v", tc.One) 414 | } 415 | 416 | // Compare 417 | if (one == two) != tc.Match { 418 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 419 | } 420 | } 421 | } 422 | 423 | func TestHash_equalSet(t *testing.T) { 424 | type Test struct { 425 | Name string 426 | Friends []string `hash:"set"` 427 | } 428 | 429 | cases := []struct { 430 | One, Two interface{} 431 | Match bool 432 | }{ 433 | { 434 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 435 | Test{Name: "foo", Friends: []string{"bar", "foo"}}, 436 | true, 437 | }, 438 | 439 | { 440 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 441 | Test{Name: "foo", Friends: []string{"foo", "bar"}}, 442 | true, 443 | }, 444 | } 445 | 446 | for _, tc := range cases { 447 | one, err := Hash(tc.One, testFormat, nil) 448 | if err != nil { 449 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 450 | } 451 | two, err := Hash(tc.Two, testFormat, nil) 452 | if err != nil { 453 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 454 | } 455 | 456 | // Zero is always wrong 457 | if one == 0 { 458 | t.Fatalf("zero hash: %#v", tc.One) 459 | } 460 | 461 | // Compare 462 | if (one == two) != tc.Match { 463 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 464 | } 465 | } 466 | } 467 | 468 | func TestHash_includable(t *testing.T) { 469 | cases := []struct { 470 | One, Two interface{} 471 | Match bool 472 | }{ 473 | { 474 | testIncludable{Value: "foo"}, 475 | testIncludable{Value: "foo"}, 476 | true, 477 | }, 478 | 479 | { 480 | testIncludable{Value: "foo", Ignore: "bar"}, 481 | testIncludable{Value: "foo"}, 482 | true, 483 | }, 484 | 485 | { 486 | testIncludable{Value: "foo", Ignore: "bar"}, 487 | testIncludable{Value: "bar"}, 488 | false, 489 | }, 490 | } 491 | 492 | for _, tc := range cases { 493 | one, err := Hash(tc.One, testFormat, nil) 494 | if err != nil { 495 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 496 | } 497 | two, err := Hash(tc.Two, testFormat, nil) 498 | if err != nil { 499 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 500 | } 501 | 502 | // Zero is always wrong 503 | if one == 0 { 504 | t.Fatalf("zero hash: %#v", tc.One) 505 | } 506 | 507 | // Compare 508 | if (one == two) != tc.Match { 509 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 510 | } 511 | } 512 | } 513 | 514 | func TestHash_ignoreZeroValue(t *testing.T) { 515 | cases := []struct { 516 | IgnoreZeroValue bool 517 | }{ 518 | { 519 | IgnoreZeroValue: true, 520 | }, 521 | { 522 | IgnoreZeroValue: false, 523 | }, 524 | } 525 | structA := struct { 526 | Foo string 527 | Bar string 528 | Map map[string]int 529 | }{ 530 | Foo: "foo", 531 | Bar: "bar", 532 | } 533 | structB := struct { 534 | Foo string 535 | Bar string 536 | Baz string 537 | Map map[string]int 538 | }{ 539 | Foo: "foo", 540 | Bar: "bar", 541 | } 542 | 543 | for _, tc := range cases { 544 | hashA, err := Hash(structA, testFormat, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 545 | if err != nil { 546 | t.Fatalf("Failed to hash %#v: %s", structA, err) 547 | } 548 | hashB, err := Hash(structB, testFormat, &HashOptions{IgnoreZeroValue: tc.IgnoreZeroValue}) 549 | if err != nil { 550 | t.Fatalf("Failed to hash %#v: %s", structB, err) 551 | } 552 | if (hashA == hashB) != tc.IgnoreZeroValue { 553 | t.Fatalf("bad, expected: %#v\n\n%d\n\n%d", tc.IgnoreZeroValue, hashA, hashB) 554 | } 555 | } 556 | } 557 | 558 | func TestHash_includableMap(t *testing.T) { 559 | cases := []struct { 560 | One, Two interface{} 561 | Match bool 562 | }{ 563 | { 564 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 565 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 566 | true, 567 | }, 568 | 569 | { 570 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 571 | testIncludableMap{Map: map[string]string{"foo": "bar"}}, 572 | true, 573 | }, 574 | 575 | { 576 | testIncludableMap{Map: map[string]string{"foo": "bar", "ignore": "true"}}, 577 | testIncludableMap{Map: map[string]string{"bar": "baz"}}, 578 | false, 579 | }, 580 | } 581 | 582 | for _, tc := range cases { 583 | one, err := Hash(tc.One, testFormat, nil) 584 | if err != nil { 585 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 586 | } 587 | two, err := Hash(tc.Two, testFormat, nil) 588 | if err != nil { 589 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 590 | } 591 | 592 | // Zero is always wrong 593 | if one == 0 { 594 | t.Fatalf("zero hash: %#v", tc.One) 595 | } 596 | 597 | // Compare 598 | if (one == two) != tc.Match { 599 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 600 | } 601 | } 602 | } 603 | 604 | func TestHash_hashable(t *testing.T) { 605 | cases := []struct { 606 | One, Two interface{} 607 | Match bool 608 | Err string 609 | }{ 610 | { 611 | testHashable{Value: "foo"}, 612 | &testHashablePointer{Value: "foo"}, 613 | true, 614 | "", 615 | }, 616 | 617 | { 618 | testHashable{Value: "foo1"}, 619 | &testHashablePointer{Value: "foo2"}, 620 | true, 621 | "", 622 | }, 623 | { 624 | testHashable{Value: "foo"}, 625 | &testHashablePointer{Value: "bar"}, 626 | false, 627 | "", 628 | }, 629 | { 630 | testHashable{Value: "nofoo"}, 631 | &testHashablePointer{Value: "bar"}, 632 | true, 633 | "", 634 | }, 635 | { 636 | testHashable{Value: "bar", Err: fmt.Errorf("oh no")}, 637 | testHashable{Value: "bar"}, 638 | true, 639 | "oh no", 640 | }, 641 | } 642 | 643 | for i, tc := range cases { 644 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 645 | one, err := Hash(tc.One, testFormat, nil) 646 | if tc.Err != "" { 647 | if err == nil { 648 | t.Fatal("expected error") 649 | } 650 | 651 | if !strings.Contains(err.Error(), tc.Err) { 652 | t.Fatalf("expected error to contain %q, got: %s", tc.Err, err) 653 | } 654 | 655 | return 656 | } 657 | if err != nil { 658 | t.Fatalf("Failed to hash %#v: %s", tc.One, err) 659 | } 660 | 661 | two, err := Hash(tc.Two, testFormat, nil) 662 | if err != nil { 663 | t.Fatalf("Failed to hash %#v: %s", tc.Two, err) 664 | } 665 | 666 | // Zero is always wrong 667 | if one == 0 { 668 | t.Fatalf("zero hash: %#v", tc.One) 669 | } 670 | 671 | // Compare 672 | if (one == two) != tc.Match { 673 | t.Fatalf("bad, expected: %#v\n\n%#v\n\n%#v", tc.Match, tc.One, tc.Two) 674 | } 675 | }) 676 | } 677 | } 678 | 679 | type testIncludable struct { 680 | Value string 681 | Ignore string 682 | } 683 | 684 | func (t testIncludable) HashInclude(field string, v interface{}) (bool, error) { 685 | return field != "Ignore", nil 686 | } 687 | 688 | type testIncludableMap struct { 689 | Map map[string]string 690 | } 691 | 692 | func (t testIncludableMap) HashIncludeMap(field string, k, v interface{}) (bool, error) { 693 | if field != "Map" { 694 | return true, nil 695 | } 696 | 697 | if s, ok := k.(string); ok && s == "ignore" { 698 | return false, nil 699 | } 700 | 701 | return true, nil 702 | } 703 | 704 | type testHashable struct { 705 | Value string 706 | Err error 707 | } 708 | 709 | func (t testHashable) Hash() (uint64, error) { 710 | if t.Err != nil { 711 | return 0, t.Err 712 | } 713 | 714 | if strings.HasPrefix(t.Value, "foo") { 715 | return 500, nil 716 | } 717 | 718 | return 100, nil 719 | } 720 | 721 | type testHashablePointer struct { 722 | Value string 723 | } 724 | 725 | func (t *testHashablePointer) Hash() (uint64, error) { 726 | if strings.HasPrefix(t.Value, "foo") { 727 | return 500, nil 728 | } 729 | 730 | return 100, nil 731 | } 732 | -------------------------------------------------------------------------------- /include.go: -------------------------------------------------------------------------------- 1 | package hashstructure 2 | 3 | // Includable is an interface that can optionally be implemented by 4 | // a struct. It will be called for each field in the struct to check whether 5 | // it should be included in the hash. 6 | type Includable interface { 7 | HashInclude(field string, v interface{}) (bool, error) 8 | } 9 | 10 | // IncludableMap is an interface that can optionally be implemented by 11 | // a struct. It will be called when a map-type field is found to ask the 12 | // struct if the map item should be included in the hash. 13 | type IncludableMap interface { 14 | HashIncludeMap(field string, k, v interface{}) (bool, error) 15 | } 16 | 17 | // Hashable is an interface that can optionally be implemented by a struct 18 | // to override the hash value. This value will override the hash value for 19 | // the entire struct. Entries in the struct will not be hashed. 20 | type Hashable interface { 21 | Hash() (uint64, error) 22 | } 23 | --------------------------------------------------------------------------------