From 3f301d79957f1be4819ade4e1fa76a462244389b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 21 Apr 2015 16:02:03 +0100 Subject: [PATCH] audit: add hashstructure --- audit/hashstructure.go | 181 ++++++++++++++++++++++++++++++++++++ audit/hashstructure_test.go | 45 +++++++++ 2 files changed, 226 insertions(+) create mode 100644 audit/hashstructure.go create mode 100644 audit/hashstructure_test.go diff --git a/audit/hashstructure.go b/audit/hashstructure.go new file mode 100644 index 0000000000..824704f6aa --- /dev/null +++ b/audit/hashstructure.go @@ -0,0 +1,181 @@ +package audit + +import ( + "fmt" + "reflect" + "strings" + + "github.com/mitchellh/copystructure" + "github.com/mitchellh/reflectwalk" +) + +// HashStructures takes an interface and hashes all the values within +// the structure. Only _values_ are hashed: keys of objects are not. +func HashStructure(s interface{}, cb HashCallback) (interface{}, error) { + s, err := copystructure.Copy(s) + if err != nil { + return nil, err + } + + walker := &hashWalker{Callback: cb} + if err := reflectwalk.Walk(s, walker); err != nil { + return nil, err + } + + return s, nil +} + +// HashCallback is the callback called for HashStructure to hash +// a value. +type HashCallback func(string) (string, error) + +// hashWalker implements interfaces for the reflectwalk package +// (github.com/mitchellh/reflectwalk) that can be used to automatically +// replace primitives with a hashed value. +type hashWalker struct { + // Callback is the function to call with the primitive that is + // to be hashed. If there is an error, walking will be halted + // immediately and the error returned. + Callback HashCallback + + key []string + lastValue reflect.Value + loc reflectwalk.Location + cs []reflect.Value + csKey []reflect.Value + csData interface{} + sliceIndex int + unknownKeys []string +} + +func (w *hashWalker) Enter(loc reflectwalk.Location) error { + w.loc = loc + return nil +} + +func (w *hashWalker) Exit(loc reflectwalk.Location) error { + w.loc = reflectwalk.None + + switch loc { + case reflectwalk.Map: + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.MapValue: + w.key = w.key[:len(w.key)-1] + w.csKey = w.csKey[:len(w.csKey)-1] + case reflectwalk.Slice: + w.cs = w.cs[:len(w.cs)-1] + case reflectwalk.SliceElem: + w.csKey = w.csKey[:len(w.csKey)-1] + } + + return nil +} + +func (w *hashWalker) Map(m reflect.Value) error { + w.cs = append(w.cs, m) + return nil +} + +func (w *hashWalker) MapElem(m, k, v reflect.Value) error { + w.csData = k + w.csKey = append(w.csKey, k) + w.key = append(w.key, k.String()) + w.lastValue = v + return nil +} + +func (w *hashWalker) Slice(s reflect.Value) error { + w.cs = append(w.cs, s) + return nil +} + +func (w *hashWalker) SliceElem(i int, elem reflect.Value) error { + w.csKey = append(w.csKey, reflect.ValueOf(i)) + w.sliceIndex = i + return nil +} + +func (w *hashWalker) Primitive(v reflect.Value) error { + if w.Callback == nil { + return nil + } + + // We don't touch map keys + if w.loc == reflectwalk.MapKey { + return nil + } + + setV := v + + // We only care about strings + if v.Kind() == reflect.Interface { + setV = v + v = v.Elem() + } + if v.Kind() != reflect.String { + return nil + } + + replaceVal, err := w.Callback(v.Interface().(string)) + if err != nil { + return fmt.Errorf("Error hashing value: %s", err) + } + + resultVal := reflect.ValueOf(replaceVal) + switch w.loc { + case reflectwalk.MapKey: + m := w.cs[len(w.cs)-1] + + // Delete the old value + var zero reflect.Value + m.SetMapIndex(w.csData.(reflect.Value), zero) + + // Set the new key with the existing value + m.SetMapIndex(resultVal, w.lastValue) + + // Set the key to be the new key + w.csData = resultVal + case reflectwalk.MapValue: + // If we're in a map, then the only way to set a map value is + // to set it directly. + m := w.cs[len(w.cs)-1] + mk := w.csData.(reflect.Value) + m.SetMapIndex(mk, resultVal) + default: + // Otherwise, we should be addressable + setV.Set(resultVal) + } + + return nil +} + +func (w *hashWalker) removeCurrent() { + // Append the key to the unknown keys + w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, ".")) + + for i := 1; i <= len(w.cs); i++ { + c := w.cs[len(w.cs)-i] + switch c.Kind() { + case reflect.Map: + // Zero value so that we delete the map key + var val reflect.Value + + // Get the key and delete it + k := w.csData.(reflect.Value) + c.SetMapIndex(k, val) + return + } + } + + panic("No container found for removeCurrent") +} + +func (w *hashWalker) replaceCurrent(v reflect.Value) { + c := w.cs[len(w.cs)-2] + switch c.Kind() { + case reflect.Map: + // Get the key and delete it + k := w.csKey[len(w.csKey)-1] + c.SetMapIndex(k, v) + } +} diff --git a/audit/hashstructure_test.go b/audit/hashstructure_test.go new file mode 100644 index 0000000000..9615790476 --- /dev/null +++ b/audit/hashstructure_test.go @@ -0,0 +1,45 @@ +package audit + +import ( + "reflect" + "testing" +) + +func TestHashWalker(t *testing.T) { + replaceText := "foo" + + cases := []struct { + Input interface{} + Output interface{} + }{ + { + map[string]interface{}{ + "hello": "foo", + }, + map[string]interface{}{ + "hello": replaceText, + }, + }, + + { + map[string]interface{}{ + "hello": []interface{}{"world"}, + }, + map[string]interface{}{ + "hello": []interface{}{replaceText}, + }, + }, + } + + for _, tc := range cases { + output, err := HashStructure(tc.Input, func(string) (string, error) { + return replaceText, nil + }) + if err != nil { + t.Fatalf("err: %s\n\n%#v", err, tc.Input) + } + if !reflect.DeepEqual(output, tc.Output) { + t.Fatalf("bad:\n\n%#v\n\n%#v", tc.Input, output) + } + } +}