diff --git a/stores/dotenv/store.go b/stores/dotenv/store.go index f805f5ce3..fc738533b 100644 --- a/stores/dotenv/store.go +++ b/stores/dotenv/store.go @@ -48,7 +48,7 @@ func (store *Store) LoadEncryptedFile(in []byte) (sops.Tree, error) { } } - metadata, err := stores.Unflatten(mdMap) + metadata, err := stores.UnflattenMetadata(mdMap) if err != nil { return sops.Tree{}, err } @@ -100,7 +100,7 @@ func (store *Store) LoadPlainFile(in []byte) (sops.TreeBranches, error) { // runtime object func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { metadata := stores.MetadataFromInternal(in.Metadata) - mdItems, err := stores.Flatten(metadata) + mdItems, err := stores.FlattenMetadata(metadata) if err != nil { return nil, err } diff --git a/stores/flatten.go b/stores/flatten.go index 994325b6b..bf5240e67 100644 --- a/stores/flatten.go +++ b/stores/flatten.go @@ -44,10 +44,10 @@ func flattenValue(value interface{}) interface{} { return output } -// flattenMap flattens a map with potentially nested maps into a flat +// Flatten flattens a map with potentially nested maps into a flat // map. Only string keys are allowed on both the top-level map and // child maps. -func flattenMap(in map[string]interface{}) map[string]interface{} { +func Flatten(in map[string]interface{}) map[string]interface{} { newMap := make(map[string]interface{}) for k, v := range in { if flat, ok := flattenValue(v).(map[string]interface{}); ok { @@ -61,7 +61,7 @@ func flattenMap(in map[string]interface{}) map[string]interface{} { return newMap } -func Flatten(md Metadata) (map[string]interface{}, error) { +func FlattenMetadata(md Metadata) (map[string]interface{}, error) { var mdMap map[string]interface{} jsonBytes, err := json.Marshal(md) if err != nil { @@ -72,14 +72,9 @@ func Flatten(md Metadata) (map[string]interface{}, error) { return nil, err } - flat := flattenMap(mdMap) - macOnlyEncryptedToString(flat) - for k, v := range flat { - if s, ok := v.(string); ok { - flat[k] = strings.Replace(s, "\n", "\\n", -1) - } - } - + flat := Flatten(mdMap) + encodeNonStrings(flat) + encodeNewLines(flat) return flat, nil } @@ -93,7 +88,7 @@ type listToken struct { position int } -// tokenize converts a path generated by flattenMap to be used as a key +// tokenize converts a path generated by Flatten to be used as a key // in the flattened map, and converts it to a slice of tokens func tokenize(path string) []token { const ( @@ -196,8 +191,8 @@ func unflatten(currentNode interface{}, currentToken, nextToken token, value int return nil } -// unflattenMap unflattens a map flattened by flattenMap -func unflattenMap(in map[string]interface{}) map[string]interface{} { +// Unflatten unflattens a map flattened by Flatten +func Unflatten(in map[string]interface{}) map[string]interface{} { newMap := make(map[string]interface{}) for k, v := range in { var current interface{} = newMap @@ -209,16 +204,11 @@ func unflattenMap(in map[string]interface{}) map[string]interface{} { return newMap } -// Unflatten unflattens a map flattened by Flatten into Metadata -func Unflatten(in map[string]interface{}) (Metadata, error) { - for k, v := range in { - if s, ok := v.(string); ok { - in[k] = strings.Replace(s, "\\n", "\n", -1) - } - } - - m := unflattenMap(in) - macOnlyEncryptedToBool(m) +// UnflattenMetadata unflattens a map flattened by FlattenMetadata into Metadata +func UnflattenMetadata(in map[string]interface{}) (Metadata, error) { + decodeNewLines(in) + decodeNonStrings(in) + m := Unflatten(in) var md Metadata jsonBytes, err := json.Marshal(m) if err != nil { @@ -228,7 +218,24 @@ func Unflatten(in map[string]interface{}) (Metadata, error) { return md, err } -func macOnlyEncryptedToBool(m map[string]interface{}) { +func decodeNewLines(m map[string]interface{}) { + for k, v := range m { + if s, ok := v.(string); ok { + m[k] = strings.Replace(s, "\\n", "\n", -1) + } + } +} + +func encodeNewLines(m map[string]interface{}) { + for k, v := range m { + if s, ok := v.(string); ok { + m[k] = strings.Replace(s, "\n", "\\n", -1) + } + } +} + +// decodeNonStrings will look for known keys that are not strings and decode to the appropriate type +func decodeNonStrings(m map[string]interface{}) { if v, ok := m["mac_only_encrypted"]; ok { m["mac_only_encrypted"] = false if v == "true" { @@ -237,7 +244,8 @@ func macOnlyEncryptedToBool(m map[string]interface{}) { } } -func macOnlyEncryptedToString(m map[string]interface{}) { +// encodeNonStrings will look for known keys that are not strings and will encode it to strings +func encodeNonStrings(m map[string]interface{}) { if v, found := m["mac_only_encrypted"]; found { if vBool, ok := v.(bool); ok { m["mac_only_encrypted"] = "false" diff --git a/stores/flatten_test.go b/stores/flatten_test.go index 7b87f4cab..40d042ba3 100644 --- a/stores/flatten_test.go +++ b/stores/flatten_test.go @@ -13,9 +13,9 @@ func TestFlat(t *testing.T) { expected := map[string]interface{}{ "foo": "bar", } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } @@ -30,9 +30,9 @@ func TestMap(t *testing.T) { "foo" + mapSeparator + "bar": 0, "foo" + mapSeparator + "baz": 0, } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } @@ -47,9 +47,9 @@ func TestFlattenMapMoreNesting(t *testing.T) { expected := map[string]interface{}{ "foo" + mapSeparator + "bar" + mapSeparator + "baz": 0, } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } @@ -62,9 +62,9 @@ func TestFlattenList(t *testing.T) { expected := map[string]interface{}{ "foo" + listSeparator + "0": 0, } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } @@ -79,13 +79,13 @@ func TestFlattenListWithMap(t *testing.T) { expected := map[string]interface{}{ "foo" + listSeparator + "0" + mapSeparator + "bar": 0, } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } -func TestFlattenMap(t *testing.T) { +func TestFlatten(t *testing.T) { input := map[string]interface{}{ "foo": "bar", "baz": map[string]interface{}{ @@ -106,9 +106,9 @@ func TestFlattenMap(t *testing.T) { "qux" + listSeparator + "1": 1, "qux" + listSeparator + "2": 2, } - flattened := flattenMap(input) + flattened := Flatten(input) assert.Equal(t, expected, flattened) - unflattened := unflattenMap(flattened) + unflattened := Unflatten(flattened) assert.Equal(t, input, unflattened) } @@ -140,38 +140,7 @@ func TestTokenizeNested(t *testing.T) { assert.Equal(t, expected, tokenized) } -func TestMacOnlyEncryptedToBool(t *testing.T) { - tests := []struct { - input map[string]interface{} - want map[string]interface{} - }{ - {map[string]interface{}{"mac_only_encrypted": "false"}, map[string]interface{}{"mac_only_encrypted": false}}, - {map[string]interface{}{"mac_only_encrypted": "true"}, map[string]interface{}{"mac_only_encrypted": true}}, - {map[string]interface{}{"mac_only_encrypted": "something-else"}, map[string]interface{}{"mac_only_encrypted": false}}, - } - - for _, tt := range tests { - macOnlyEncryptedToBool(tt.input) - assert.Equal(t, tt.want, tt.input) - } -} - -func TestMacOnlyEncryptedToString(t *testing.T) { - tests := []struct { - input map[string]interface{} - want map[string]interface{} - }{ - {map[string]interface{}{"mac_only_encrypted": false}, map[string]interface{}{"mac_only_encrypted": "false"}}, - {map[string]interface{}{"mac_only_encrypted": true}, map[string]interface{}{"mac_only_encrypted": "true"}}, - } - - for _, tt := range tests { - macOnlyEncryptedToString(tt.input) - assert.Equal(t, tt.want, tt.input) - } -} - -func TestFlatten(t *testing.T) { +func TestFlattenMetadata(t *testing.T) { tests := []struct { input Metadata want map[string]interface{} @@ -183,7 +152,7 @@ func TestFlatten(t *testing.T) { } for _, tt := range tests { - got, err := Flatten(tt.input) + got, err := FlattenMetadata(tt.input) assert.NoError(t, err) for k, v := range tt.want { assert.Equal(t, v, got[k]) @@ -191,7 +160,7 @@ func TestFlatten(t *testing.T) { } } -func TestFlattenToUnflatten(t *testing.T) { +func TestFlattenMetadataToUnflattenMetadata(t *testing.T) { tests := []struct { input Metadata }{ @@ -203,10 +172,75 @@ func TestFlattenToUnflatten(t *testing.T) { } for _, tt := range tests { - flat, err := Flatten(tt.input) + flat, err := FlattenMetadata(tt.input) assert.NoError(t, err) - md, err := Unflatten(flat) + md, err := UnflattenMetadata(flat) assert.NoError(t, err) assert.Equal(t, tt.input, md) } } + +func TestDecodeNewLines(t *testing.T) { + tests := []struct { + input map[string]interface{} + want map[string]interface{} + }{ + {map[string]interface{}{"mac": "line1\\nline2"}, map[string]interface{}{"mac": "line1\nline2"}}, + {map[string]interface{}{"mac": "line1\\n\\n\\nline2\\n\\nline3"}, map[string]interface{}{"mac": "line1\n\n\nline2\n\nline3"}}, + } + + for _, tt := range tests { + decodeNewLines(tt.input) + for k, v := range tt.want { + assert.Equal(t, v, tt.input[k]) + } + } +} + +func TestEncodeNewLines(t *testing.T) { + tests := []struct { + input map[string]interface{} + want map[string]interface{} + }{ + {map[string]interface{}{"mac": "line1\nline2"}, map[string]interface{}{"mac": "line1\\nline2"}}, + {map[string]interface{}{"mac": "line1\n\n\nline2\n\nline3"}, map[string]interface{}{"mac": "line1\\n\\n\\nline2\\n\\nline3"}}, + } + + for _, tt := range tests { + encodeNewLines(tt.input) + for k, v := range tt.want { + assert.Equal(t, v, tt.input[k]) + } + } +} + +func TestDecodeNonStrings(t *testing.T) { + tests := []struct { + input map[string]interface{} + want map[string]interface{} + }{ + {map[string]interface{}{"mac_only_encrypted": "false"}, map[string]interface{}{"mac_only_encrypted": false}}, + {map[string]interface{}{"mac_only_encrypted": "true"}, map[string]interface{}{"mac_only_encrypted": true}}, + {map[string]interface{}{"mac_only_encrypted": "something-else"}, map[string]interface{}{"mac_only_encrypted": false}}, + } + + for _, tt := range tests { + decodeNonStrings(tt.input) + assert.Equal(t, tt.want, tt.input) + } +} + +func TestEncodeNonStrings(t *testing.T) { + tests := []struct { + input map[string]interface{} + want map[string]interface{} + }{ + {map[string]interface{}{"mac_only_encrypted": false}, map[string]interface{}{"mac_only_encrypted": "false"}}, + {map[string]interface{}{"mac_only_encrypted": true}, map[string]interface{}{"mac_only_encrypted": "true"}}, + } + + for _, tt := range tests { + encodeNonStrings(tt.input) + assert.Equal(t, tt.want, tt.input) + } +} diff --git a/stores/ini/store.go b/stores/ini/store.go index f7a571f6d..c100e3be8 100644 --- a/stores/ini/store.go +++ b/stores/ini/store.go @@ -187,7 +187,7 @@ func (store *Store) iniSectionToMetadata(sopsSection *ini.Section) (stores.Metad for k, v := range sopsSection.KeysHash() { metadataHash[k] = v } - return stores.Unflatten(metadataHash) + return stores.UnflattenMetadata(metadataHash) } // LoadPlainFile loads a plaintext INI file's bytes onto a sops.TreeBranches runtime object @@ -221,7 +221,7 @@ func (store *Store) EmitEncryptedFile(in sops.Tree) ([]byte, error) { } func (store *Store) encodeMetadataToIniBranch(md stores.Metadata) (sops.TreeBranch, error) { - flat, err := stores.Flatten(md) + flat, err := stores.FlattenMetadata(md) if err != nil { return nil, err }