7c8e6676c0
* return error from getRuleInfo if rule contains empty slice to prevent panic * add changelog entry
156 lines
4 KiB
Go
156 lines
4 KiB
Go
package random
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
// ParsePolicy is a convenience function for parsing HCL into a StringGenerator.
|
|
// See PolicyParser.ParsePolicy for details.
|
|
func ParsePolicy(raw string) (gen StringGenerator, err error) {
|
|
parser := PolicyParser{
|
|
RuleRegistry: Registry{
|
|
Rules: defaultRuleNameMapping,
|
|
},
|
|
}
|
|
return parser.ParsePolicy(raw)
|
|
}
|
|
|
|
// ParsePolicyBytes is a convenience function for parsing HCL into a StringGenerator.
|
|
// See PolicyParser.ParsePolicy for details.
|
|
func ParsePolicyBytes(raw []byte) (gen StringGenerator, err error) {
|
|
return ParsePolicy(string(raw))
|
|
}
|
|
|
|
// PolicyParser parses string generator configuration from HCL.
|
|
type PolicyParser struct {
|
|
// RuleRegistry maps rule names in HCL to Rule constructors.
|
|
RuleRegistry Registry
|
|
}
|
|
|
|
// ParsePolicy parses the provided HCL into a StringGenerator.
|
|
func (p PolicyParser) ParsePolicy(raw string) (sg StringGenerator, err error) {
|
|
rawData := map[string]interface{}{}
|
|
err = hcl.Decode(&rawData, raw)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to decode: %w", err)
|
|
}
|
|
|
|
// Decode the top level items
|
|
gen := StringGenerator{}
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
Result: &gen,
|
|
DecodeHook: stringToRunesFunc,
|
|
})
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to decode configuration: %w", err)
|
|
}
|
|
|
|
err = decoder.Decode(rawData)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("failed to decode configuration: %w", err)
|
|
}
|
|
|
|
// Decode & parse rules
|
|
rawRules, err := getMapSlice(rawData, "rule")
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to retrieve rules: %w", err)
|
|
}
|
|
|
|
rules, err := parseRules(p.RuleRegistry, rawRules)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to parse rules: %w", err)
|
|
}
|
|
|
|
gen = StringGenerator{
|
|
Length: gen.Length,
|
|
Rules: rules,
|
|
}
|
|
|
|
err = gen.validateConfig()
|
|
if err != nil {
|
|
return sg, err
|
|
}
|
|
|
|
return gen, nil
|
|
}
|
|
|
|
func parseRules(registry Registry, rawRules []map[string]interface{}) (rules []Rule, err error) {
|
|
for _, rawRule := range rawRules {
|
|
info, err := getRuleInfo(rawRule)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get rule info: %w", err)
|
|
}
|
|
|
|
rule, err := registry.parseRule(info.ruleType, info.data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse rule %s: %w", info.ruleType, err)
|
|
}
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
return rules, nil
|
|
}
|
|
|
|
// getMapSlice from the provided map. This will retrieve and type-assert a []map[string]interface{} from the map
|
|
// This will not error if the key does not exist
|
|
// This will return an error if the value at the provided key is not of type []map[string]interface{}
|
|
func getMapSlice(m map[string]interface{}, key string) (mapSlice []map[string]interface{}, err error) {
|
|
rawSlice, exists := m[key]
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
|
|
mapSlice = []map[string]interface{}{}
|
|
err = mapstructure.Decode(rawSlice, &mapSlice)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mapSlice, nil
|
|
}
|
|
|
|
type ruleInfo struct {
|
|
ruleType string
|
|
data map[string]interface{}
|
|
}
|
|
|
|
// getRuleInfo splits the provided HCL-decoded rule into its rule type along with the data associated with it
|
|
func getRuleInfo(rule map[string]interface{}) (data ruleInfo, err error) {
|
|
// There should only be one key, but it's a dynamic key yay!
|
|
for key := range rule {
|
|
slice, err := getMapSlice(rule, key)
|
|
if err != nil {
|
|
return data, fmt.Errorf("unable to get rule data: %w", err)
|
|
}
|
|
|
|
if len(slice) == 0 {
|
|
return data, fmt.Errorf("rule info cannot be empty")
|
|
}
|
|
|
|
data = ruleInfo{
|
|
ruleType: key,
|
|
data: slice[0],
|
|
}
|
|
return data, nil
|
|
}
|
|
return data, fmt.Errorf("rule is empty")
|
|
}
|
|
|
|
// stringToRunesFunc converts a string to a []rune for use in the mapstructure library
|
|
func stringToRunesFunc(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
|
|
if from != reflect.String || to != reflect.Slice {
|
|
return data, nil
|
|
}
|
|
|
|
raw := data.(string)
|
|
|
|
if !utf8.ValidString(raw) {
|
|
return nil, fmt.Errorf("invalid UTF8 string")
|
|
}
|
|
return []rune(raw), nil
|
|
}
|