1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00
Files
sops/stores/flatten.go

258 lines
6.6 KiB
Go
Raw Normal View History

package stores
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
const mapSeparator = "__map_"
const listSeparator = "__list_"
// flattenAndMerge flattens the provided value and merges into the
// into map using prefix
func flattenAndMerge(into map[string]interface{}, prefix string, value interface{}) {
flattenedValue := flattenValue(value)
if flattenedValue, ok := flattenedValue.(map[string]interface{}); ok {
for flatK, flatV := range flattenedValue {
into[prefix+flatK] = flatV
}
} else {
into[prefix] = value
}
}
func flattenValue(value interface{}) interface{} {
var output interface{}
switch value := value.(type) {
case map[string]interface{}:
newMap := make(map[string]interface{})
for k, v := range value {
flattenAndMerge(newMap, mapSeparator+k, v)
}
output = newMap
case []interface{}:
newMap := make(map[string]interface{})
for i, v := range value {
flattenAndMerge(newMap, listSeparator+fmt.Sprintf("%d", i), v)
}
output = newMap
default:
output = value
}
return output
}
// 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 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 {
for flatK, flatV := range flat {
newMap[k+flatK] = flatV
}
} else {
newMap[k] = v
}
}
return newMap
}
func FlattenMetadata(md Metadata) (map[string]interface{}, error) {
var mdMap map[string]interface{}
jsonBytes, err := json.Marshal(md)
if err != nil {
return nil, err
}
err = json.Unmarshal(jsonBytes, &mdMap)
if err != nil {
return nil, err
}
flat := Flatten(mdMap)
encodeNonStrings(flat)
encodeNewLines(flat)
return flat, nil
}
type token interface{}
type mapToken struct {
key string
}
type listToken struct {
position int
}
// 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 (
StateNormal = 0
StateMap = iota
StateList = iota
)
var tokens []token
state := StateNormal
lastTokenEnd := 0
i := 0
finishPrevToken := func() {
var t token
switch state {
case StateNormal:
t = mapToken{path[lastTokenEnd:i]}
case StateMap:
t = mapToken{path[lastTokenEnd+len(mapSeparator) : i]}
case StateList:
pos, _ := strconv.Atoi(path[lastTokenEnd+len(listSeparator) : i])
t = listToken{pos}
}
lastTokenEnd = i
tokens = append(tokens, t)
}
for i < len(path) {
if strings.HasPrefix(path[i:], mapSeparator) {
finishPrevToken()
state = StateMap
i += len(mapSeparator)
} else if strings.HasPrefix(path[i:], listSeparator) {
finishPrevToken()
state = StateList
i += len(listSeparator)
} else {
2019-07-08 15:32:33 -07:00
i++
}
}
finishPrevToken()
return tokens
}
// unflatten takes the currentNode, currentToken, nextToken and value
// and populates currentNode such that currentToken can be considered
// processed. It inspects nextToken to decide what type to allocate
// and assign under currentNode.
func unflatten(currentNode interface{}, currentToken, nextToken token, value interface{}) interface{} {
switch currentToken := currentToken.(type) {
case mapToken:
currentNode := currentNode.(map[string]interface{})
switch nextToken := nextToken.(type) {
case mapToken:
if _, ok := currentNode[currentToken.key]; !ok {
currentNode[currentToken.key] = make(map[string]interface{})
}
next := currentNode[currentToken.key].(map[string]interface{})
return next
case listToken:
if _, ok := currentNode[currentToken.key]; !ok {
currentNode[currentToken.key] = make([]interface{}, nextToken.position+1)
}
next := currentNode[currentToken.key].([]interface{})
if nextToken.position >= len(next) {
// Grow the slice and reassign it
newNext := make([]interface{}, nextToken.position+1)
copy(newNext, next)
next = newNext
currentNode[currentToken.key] = next
}
return next
default:
currentNode[currentToken.key] = value
}
case listToken:
currentNode := currentNode.([]interface{})
switch nextToken := nextToken.(type) {
case mapToken:
if currentNode[currentToken.position] == nil {
currentNode[currentToken.position] = make(map[string]interface{})
}
next := currentNode[currentToken.position].(map[string]interface{})
return next
case listToken:
if currentNode[currentToken.position] == nil {
currentNode[currentToken.position] = make([]interface{}, nextToken.position+1)
}
next := currentNode[currentToken.position].([]interface{})
if nextToken.position >= len(next) {
// Grow the slice and reassign it
newNext := make([]interface{}, nextToken.position+1)
copy(newNext, next)
next = newNext
currentNode[currentToken.position] = next
}
return next
default:
currentNode[currentToken.position] = value
}
}
return nil
}
// 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
tokens := append(tokenize(k), nil)
for i := 0; i < len(tokens)-1; i++ {
current = unflatten(current, tokens[i], tokens[i+1], v)
}
}
return newMap
}
// 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 {
return md, err
}
err = json.Unmarshal(jsonBytes, &md)
return md, err
}
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" {
m["mac_only_encrypted"] = true
}
}
}
// 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"
if vBool {
m["mac_only_encrypted"] = "true"
}
}
}
}