mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-03 02:10:18 +00:00
736 lines
18 KiB
Go
736 lines
18 KiB
Go
package schema
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
exprparser "gitea.com/gitea/act_runner/internal/expr"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
//go:embed workflow_schema.json
|
|
var workflowSchema string
|
|
|
|
//go:embed action_schema.json
|
|
var actionSchema string
|
|
|
|
var functions = regexp.MustCompile(`^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$`)
|
|
|
|
type ValidationKind int
|
|
|
|
const (
|
|
ValidationKindFatal ValidationKind = iota
|
|
ValidationKindWarning
|
|
ValidationKindInvalidProperty
|
|
ValidationKindMismatched
|
|
ValidationKindMissingProperty
|
|
)
|
|
|
|
type Location struct {
|
|
Line int
|
|
Column int
|
|
}
|
|
|
|
type ValidationError struct {
|
|
Kind ValidationKind
|
|
Location
|
|
Message string
|
|
}
|
|
|
|
func (e ValidationError) Error() string {
|
|
return fmt.Sprintf("Line: %d Column %d: %s", e.Line, e.Column, e.Message)
|
|
}
|
|
|
|
type ValidationErrorCollection struct {
|
|
Errors []ValidationError
|
|
Collections []ValidationErrorCollection
|
|
}
|
|
|
|
func indent(builder *strings.Builder, in string) {
|
|
for v := range strings.SplitSeq(in, "\n") {
|
|
if v != "" {
|
|
builder.WriteString(" ")
|
|
builder.WriteString(v)
|
|
}
|
|
builder.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
func (c ValidationErrorCollection) Error() string {
|
|
var builder strings.Builder
|
|
for _, e := range c.Errors {
|
|
if builder.Len() > 0 {
|
|
builder.WriteString("\n")
|
|
}
|
|
builder.WriteString(e.Error())
|
|
}
|
|
for _, e := range c.Collections {
|
|
if builder.Len() > 0 {
|
|
builder.WriteString("\n")
|
|
}
|
|
indent(&builder, e.Error())
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
func (c *ValidationErrorCollection) AddError(err ValidationError) {
|
|
c.Errors = append(c.Errors, err)
|
|
}
|
|
|
|
func AsValidationErrorCollection(err error) *ValidationErrorCollection {
|
|
if col, ok := err.(ValidationErrorCollection); ok {
|
|
return &col
|
|
}
|
|
if col, ok := err.(*ValidationErrorCollection); ok {
|
|
return col
|
|
}
|
|
if e, ok := err.(ValidationError); ok {
|
|
return &ValidationErrorCollection{
|
|
Errors: []ValidationError{e},
|
|
}
|
|
}
|
|
if e, ok := err.(*ValidationError); ok {
|
|
return &ValidationErrorCollection{
|
|
Errors: []ValidationError{*e},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type Schema struct {
|
|
Definitions map[string]Definition
|
|
}
|
|
|
|
func (s *Schema) GetDefinition(name string) Definition {
|
|
def, ok := s.Definitions[name]
|
|
if !ok {
|
|
switch name {
|
|
case "any":
|
|
return Definition{OneOf: &[]string{"sequence", "mapping", "number", "boolean", "string", "null"}}
|
|
case "sequence":
|
|
return Definition{Sequence: &SequenceDefinition{ItemType: "any"}}
|
|
case "mapping":
|
|
return Definition{Mapping: &MappingDefinition{LooseKeyType: "any", LooseValueType: "any"}}
|
|
case "number":
|
|
return Definition{Number: &NumberDefinition{}}
|
|
case "string":
|
|
return Definition{String: &StringDefinition{}}
|
|
case "boolean":
|
|
return Definition{Boolean: &BooleanDefinition{}}
|
|
case "null":
|
|
return Definition{Null: &NullDefinition{}}
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
type Definition struct {
|
|
Context []string `json:"context,omitempty"`
|
|
Mapping *MappingDefinition `json:"mapping,omitempty"`
|
|
Sequence *SequenceDefinition `json:"sequence,omitempty"`
|
|
OneOf *[]string `json:"one-of,omitempty"`
|
|
AllowedValues *[]string `json:"allowed-values,omitempty"`
|
|
String *StringDefinition `json:"string,omitempty"`
|
|
Number *NumberDefinition `json:"number,omitempty"`
|
|
Boolean *BooleanDefinition `json:"boolean,omitempty"`
|
|
Null *NullDefinition `json:"null,omitempty"`
|
|
}
|
|
|
|
type MappingDefinition struct {
|
|
Properties map[string]MappingProperty `json:"properties,omitempty"`
|
|
LooseKeyType string `json:"loose-key-type,omitempty"`
|
|
LooseValueType string `json:"loose-value-type,omitempty"`
|
|
}
|
|
|
|
type MappingProperty struct {
|
|
Type string `json:"type,omitempty"`
|
|
Required bool `json:"required,omitempty"`
|
|
}
|
|
|
|
func (s *MappingProperty) UnmarshalJSON(data []byte) error {
|
|
if json.Unmarshal(data, &s.Type) != nil {
|
|
type MProp MappingProperty
|
|
return json.Unmarshal(data, (*MProp)(s))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type SequenceDefinition struct {
|
|
ItemType string `json:"item-type"`
|
|
}
|
|
|
|
type StringDefinition struct {
|
|
Constant string `json:"constant,omitempty"`
|
|
IsExpression bool `json:"is-expression,omitempty"`
|
|
}
|
|
|
|
type NumberDefinition struct{}
|
|
|
|
type BooleanDefinition struct{}
|
|
|
|
type NullDefinition struct{}
|
|
|
|
func GetWorkflowSchema() *Schema {
|
|
sh := &Schema{}
|
|
_ = json.Unmarshal([]byte(workflowSchema), sh)
|
|
return sh
|
|
}
|
|
|
|
func GetActionSchema() *Schema {
|
|
sh := &Schema{}
|
|
_ = json.Unmarshal([]byte(actionSchema), sh)
|
|
return sh
|
|
}
|
|
|
|
type Node struct {
|
|
RestrictEval bool
|
|
Definition string
|
|
Schema *Schema
|
|
Context []string
|
|
}
|
|
|
|
type FunctionInfo interface {
|
|
GetName() string
|
|
Check(args []exprparser.Node) error
|
|
}
|
|
|
|
type BasicFunctionInfo struct {
|
|
Name string
|
|
Min int
|
|
Max int
|
|
}
|
|
|
|
func (f BasicFunctionInfo) GetName() string {
|
|
return f.Name
|
|
}
|
|
|
|
func (f BasicFunctionInfo) Check(args []exprparser.Node) error {
|
|
var err error
|
|
if f.Min > len(args) {
|
|
err = errors.Join(err, fmt.Errorf("missing parameters for %s expected >= %v got %v", f.Name, f.Min, len(args)))
|
|
}
|
|
if f.Max < len(args) {
|
|
err = errors.Join(err, fmt.Errorf("too many parameters for %s expected <= %v got %v", f.Name, f.Max, len(args)))
|
|
}
|
|
return err
|
|
}
|
|
|
|
type OddFunctionInfo struct {
|
|
BasicFunctionInfo
|
|
}
|
|
|
|
func (f OddFunctionInfo) Check(args []exprparser.Node) error {
|
|
var err error
|
|
if len(args)%2 == 0 {
|
|
err = errors.Join(err, fmt.Errorf("expected odd number of parameters for %s got %v", f.Name, len(args)))
|
|
}
|
|
return errors.Join(err, f.BasicFunctionInfo.Check(args))
|
|
}
|
|
|
|
func (s *Node) checkSingleExpression(exprNode exprparser.Node) error {
|
|
if len(s.Context) == 0 {
|
|
switch exprNode.(type) {
|
|
case *exprparser.ValueNode:
|
|
return nil
|
|
default:
|
|
return errors.New("expressions are not allowed here")
|
|
}
|
|
}
|
|
|
|
funcs := s.GetFunctions()
|
|
|
|
var err error
|
|
exprparser.VisitNode(exprNode, func(node exprparser.Node) {
|
|
if funcCallNode, ok := node.(*exprparser.FunctionNode); ok {
|
|
for _, v := range funcs {
|
|
if strings.EqualFold(funcCallNode.Name, v.GetName()) {
|
|
err = v.Check(funcCallNode.Args)
|
|
return
|
|
}
|
|
}
|
|
err = errors.Join(err, fmt.Errorf("unknown Function Call %s", funcCallNode.Name))
|
|
}
|
|
if varNode, ok := node.(*exprparser.ValueNode); ok && varNode.Kind == exprparser.TokenKindNamedValue {
|
|
if str, ok := varNode.Value.(string); ok {
|
|
for _, v := range s.Context {
|
|
if strings.EqualFold(str, v) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
err = errors.Join(err, fmt.Errorf("unknown Variable Access %v", varNode.Value))
|
|
}
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *Node) GetFunctions() []FunctionInfo {
|
|
funcs := []FunctionInfo{}
|
|
AddFunction(&funcs, "contains", 2, 2)
|
|
AddFunction(&funcs, "endsWith", 2, 2)
|
|
AddFunction(&funcs, "format", 1, 255)
|
|
AddFunction(&funcs, "join", 1, 2)
|
|
AddFunction(&funcs, "startsWith", 2, 2)
|
|
AddFunction(&funcs, "toJson", 1, 1)
|
|
AddFunction(&funcs, "fromJson", 1, 1)
|
|
funcs = append(funcs, &OddFunctionInfo{
|
|
BasicFunctionInfo: BasicFunctionInfo{
|
|
Name: "case",
|
|
Min: 3,
|
|
Max: 255,
|
|
},
|
|
})
|
|
for _, v := range s.Context {
|
|
found := strings.Contains(v, "(")
|
|
if !found {
|
|
continue
|
|
}
|
|
smatch := functions.FindStringSubmatch(v)
|
|
if len(smatch) > 0 {
|
|
functionName := smatch[1]
|
|
minParameters, _ := strconv.ParseInt(smatch[2], 10, 32)
|
|
maxParametersRaw := smatch[3]
|
|
var maxParameters int64
|
|
if strings.EqualFold(maxParametersRaw, "MAX") {
|
|
maxParameters = math.MaxInt32
|
|
} else {
|
|
maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
|
|
}
|
|
funcs = append(funcs, &BasicFunctionInfo{
|
|
Name: functionName,
|
|
Min: int(minParameters),
|
|
Max: int(maxParameters),
|
|
})
|
|
}
|
|
}
|
|
return funcs
|
|
}
|
|
|
|
func exprEnd(expr string) int {
|
|
var inQuotes bool
|
|
for i, v := range expr {
|
|
if v == '\'' {
|
|
inQuotes = !inQuotes
|
|
} else if !inQuotes && i+1 < len(expr) && expr[i:i+2] == "}}" {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (s *Node) checkExpression(node *yaml.Node) (bool, error) {
|
|
if s.RestrictEval {
|
|
return false, nil
|
|
}
|
|
val := node.Value
|
|
hadExpr := false
|
|
var err error
|
|
for {
|
|
if i := strings.Index(val, "${{"); i != -1 {
|
|
val = val[i+3:]
|
|
} else {
|
|
return hadExpr, err
|
|
}
|
|
hadExpr = true
|
|
|
|
j := exprEnd(val)
|
|
|
|
exprNode, parseErr := exprparser.Parse(val[:j])
|
|
if parseErr != nil {
|
|
err = errors.Join(err, ValidationError{
|
|
Location: toLocation(node),
|
|
Message: "failed to parse: " + parseErr.Error(),
|
|
})
|
|
continue
|
|
}
|
|
val = val[j+2:]
|
|
cerr := s.checkSingleExpression(exprNode)
|
|
if cerr != nil {
|
|
err = errors.Join(err, ValidationError{
|
|
Location: toLocation(node),
|
|
Message: cerr.Error(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
|
|
*funcs = append(*funcs, &BasicFunctionInfo{
|
|
Name: s,
|
|
Min: i1,
|
|
Max: i2,
|
|
})
|
|
}
|
|
|
|
func (s *Node) UnmarshalYAML(node *yaml.Node) error {
|
|
if node != nil && node.Kind == yaml.DocumentNode {
|
|
return s.UnmarshalYAML(node.Content[0])
|
|
}
|
|
def := s.Schema.GetDefinition(s.Definition)
|
|
if s.Context == nil {
|
|
s.Context = def.Context
|
|
}
|
|
|
|
isExpr, err := s.checkExpression(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isExpr {
|
|
return nil
|
|
}
|
|
if def.Mapping != nil {
|
|
return s.checkMapping(node, def)
|
|
} else if def.Sequence != nil {
|
|
return s.checkSequence(node, def)
|
|
} else if def.OneOf != nil {
|
|
return s.checkOneOf(def, node)
|
|
}
|
|
|
|
if err := assertKind(node, yaml.ScalarNode); err != nil {
|
|
return err
|
|
}
|
|
|
|
if def.String != nil {
|
|
return s.checkString(node, def)
|
|
} else if def.Number != nil {
|
|
var num float64
|
|
return node.Decode(&num)
|
|
} else if def.Boolean != nil {
|
|
var b bool
|
|
return node.Decode(&b)
|
|
} else if def.AllowedValues != nil {
|
|
s := node.Value
|
|
if slices.Contains(*def.AllowedValues, s) {
|
|
return nil
|
|
}
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Message: fmt.Sprintf("expected one of %s got %s", strings.Join(*def.AllowedValues, ","), s),
|
|
}
|
|
} else if def.Null != nil {
|
|
var myNull *byte
|
|
if err := node.Decode(&myNull); err != nil {
|
|
return err
|
|
}
|
|
if myNull != nil {
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Message: "invalid Null",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return errors.ErrUnsupported
|
|
}
|
|
|
|
func (s *Node) checkString(node *yaml.Node, def Definition) error {
|
|
// caller checks node type
|
|
val := node.Value
|
|
if def.String.Constant != "" && def.String.Constant != val {
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Message: fmt.Sprintf("expected %s got %s", def.String.Constant, val),
|
|
}
|
|
}
|
|
if def.String.IsExpression && !s.RestrictEval {
|
|
exprNode, parseErr := exprparser.Parse(node.Value)
|
|
if parseErr != nil {
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Message: "failed to parse: " + parseErr.Error(),
|
|
}
|
|
}
|
|
cerr := s.checkSingleExpression(exprNode)
|
|
if cerr != nil {
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Message: cerr.Error(),
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Node) checkOneOf(def Definition, node *yaml.Node) error {
|
|
invalidProps := math.MaxInt
|
|
var bestMatches ValidationErrorCollection
|
|
for _, v := range *def.OneOf {
|
|
// Use helper to create child node
|
|
sub := s.childNode(v)
|
|
err := sub.UnmarshalYAML(node)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if col := AsValidationErrorCollection(err); col != nil {
|
|
var matched int
|
|
for _, e := range col.Errors {
|
|
if e.Kind == ValidationKindInvalidProperty {
|
|
matched++
|
|
}
|
|
if e.Kind == ValidationKindMismatched {
|
|
if math.MaxInt == invalidProps {
|
|
bestMatches.Collections = append(bestMatches.Collections, *col)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if matched <= invalidProps {
|
|
if matched < invalidProps {
|
|
// clear, we have better matching ones
|
|
bestMatches.Collections = nil
|
|
}
|
|
bestMatches.Collections = append(bestMatches.Collections, *col)
|
|
invalidProps = matched
|
|
}
|
|
continue
|
|
}
|
|
bestMatches.Errors = append(bestMatches.Errors, ValidationError{
|
|
Location: toLocation(node),
|
|
Message: fmt.Sprintf("failed to match %s: %s", v, err.Error()),
|
|
})
|
|
}
|
|
if len(bestMatches.Errors) > 0 || len(bestMatches.Collections) > 0 {
|
|
return bestMatches
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getStringKind(k yaml.Kind) string {
|
|
switch k {
|
|
case yaml.DocumentNode:
|
|
return "document"
|
|
case yaml.SequenceNode:
|
|
return "sequence"
|
|
case yaml.MappingNode:
|
|
return "mapping"
|
|
case yaml.ScalarNode:
|
|
return "scalar"
|
|
case yaml.AliasNode:
|
|
return "alias"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func (s *Node) checkSequence(node *yaml.Node, def Definition) error {
|
|
if err := assertKind(node, yaml.SequenceNode); err != nil {
|
|
return err
|
|
}
|
|
var allErrors error
|
|
for _, v := range node.Content {
|
|
// Use helper to create child node
|
|
child := s.childNode(def.Sequence.ItemType)
|
|
allErrors = errors.Join(allErrors, child.UnmarshalYAML(v))
|
|
}
|
|
return allErrors
|
|
}
|
|
|
|
func toLocation(node *yaml.Node) Location {
|
|
return Location{Line: node.Line, Column: node.Column}
|
|
}
|
|
|
|
func assertKind(node *yaml.Node, kind yaml.Kind) error {
|
|
if node.Kind != kind {
|
|
return ValidationError{
|
|
Location: toLocation(node),
|
|
Kind: ValidationKindMismatched,
|
|
Message: fmt.Sprintf("expected a %s got %s", getStringKind(kind), getStringKind(node.Kind)),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Node) GetNestedNode(path ...string) *Node {
|
|
if len(path) == 0 {
|
|
return s
|
|
}
|
|
def := s.Schema.GetDefinition(s.Definition)
|
|
if def.Mapping != nil {
|
|
prop, ok := def.Mapping.Properties[path[0]]
|
|
if !ok {
|
|
if def.Mapping.LooseValueType == "" {
|
|
return nil
|
|
}
|
|
return s.childNode(def.Mapping.LooseValueType).GetNestedNode(path[1:]...)
|
|
}
|
|
return s.childNode(prop.Type).GetNestedNode(path[1:]...)
|
|
}
|
|
if def.Sequence != nil {
|
|
// OneOf Branching
|
|
if path[0] != "*" {
|
|
return nil
|
|
}
|
|
return s.childNode(def.Sequence.ItemType).GetNestedNode(path[1:]...)
|
|
}
|
|
if def.OneOf != nil {
|
|
for _, one := range *def.OneOf {
|
|
opt := s.childNode(one).GetNestedNode(path...)
|
|
if opt != nil {
|
|
return opt
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Node) checkMapping(node *yaml.Node, def Definition) error {
|
|
if err := assertKind(node, yaml.MappingNode); err != nil {
|
|
return err
|
|
}
|
|
insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
|
|
var allErrors ValidationErrorCollection
|
|
var hasKeyExpr bool
|
|
usedProperties := map[string]string{}
|
|
for i, k := range node.Content {
|
|
if i%2 == 0 {
|
|
if insertDirective.MatchString(k.Value) {
|
|
if len(s.Context) == 0 {
|
|
allErrors.AddError(ValidationError{
|
|
Location: toLocation(node),
|
|
Message: "insert is not allowed here",
|
|
})
|
|
}
|
|
hasKeyExpr = true
|
|
continue
|
|
}
|
|
|
|
isExpr, err := s.checkExpression(k)
|
|
if err != nil {
|
|
allErrors.AddError(ValidationError{
|
|
Location: toLocation(node),
|
|
Message: err.Error(),
|
|
})
|
|
hasKeyExpr = true
|
|
continue
|
|
}
|
|
if isExpr {
|
|
hasKeyExpr = true
|
|
continue
|
|
}
|
|
if org, ok := usedProperties[strings.ToLower(k.Value)]; !ok {
|
|
// duplicate check case insensitive
|
|
usedProperties[strings.ToLower(k.Value)] = k.Value
|
|
// schema check case sensitive
|
|
usedProperties[k.Value] = k.Value
|
|
} else {
|
|
allErrors.AddError(ValidationError{
|
|
// Kind: ValidationKindInvalidProperty,
|
|
Location: toLocation(node),
|
|
Message: fmt.Sprintf("duplicate property %v of %v", k.Value, org),
|
|
})
|
|
}
|
|
vdef, ok := def.Mapping.Properties[k.Value]
|
|
if !ok {
|
|
if def.Mapping.LooseValueType == "" {
|
|
allErrors.AddError(ValidationError{
|
|
Kind: ValidationKindInvalidProperty,
|
|
Location: toLocation(node),
|
|
Message: fmt.Sprintf("unknown property %v", k.Value),
|
|
})
|
|
continue
|
|
}
|
|
vdef = MappingProperty{Type: def.Mapping.LooseValueType}
|
|
}
|
|
|
|
// Use helper to create child node
|
|
child := s.childNode(vdef.Type)
|
|
if err := child.UnmarshalYAML(node.Content[i+1]); err != nil {
|
|
if col := AsValidationErrorCollection(err); col != nil {
|
|
allErrors.AddError(ValidationError{
|
|
Location: toLocation(node.Content[i+1]),
|
|
Message: "error found in value of key " + k.Value,
|
|
})
|
|
allErrors.Collections = append(allErrors.Collections, *col)
|
|
continue
|
|
}
|
|
allErrors.AddError(ValidationError{
|
|
Location: toLocation(node),
|
|
Message: err.Error(),
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
if !hasKeyExpr {
|
|
for k, v := range def.Mapping.Properties {
|
|
if _, ok := usedProperties[k]; !ok && v.Required {
|
|
allErrors.AddError(ValidationError{
|
|
Location: toLocation(node),
|
|
Kind: ValidationKindMissingProperty,
|
|
Message: "missing property " + k,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
if len(allErrors.Errors) == 0 && len(allErrors.Collections) == 0 {
|
|
return nil
|
|
}
|
|
return allErrors
|
|
}
|
|
|
|
func (s *Node) childNode(defName string) *Node {
|
|
return &Node{
|
|
RestrictEval: s.RestrictEval,
|
|
Definition: defName,
|
|
Schema: s.Schema,
|
|
Context: append(append([]string{}, s.Context...), s.Schema.GetDefinition(defName).Context...),
|
|
}
|
|
}
|
|
|
|
func (s *Node) GetVariables() []string {
|
|
// Return only variable names (exclude function signatures)
|
|
vars := []string{}
|
|
for _, v := range s.Context {
|
|
if !strings.Contains(v, "(") {
|
|
vars = append(vars, v)
|
|
}
|
|
}
|
|
return vars
|
|
}
|
|
|
|
// ValidateExpression checks whether all variables and functions used in the expressions
|
|
// inside the provided yaml.Node are present in the allowed sets. It returns false
|
|
// if any variable or function is missing.
|
|
func (s *Node) ValidateExpression(node *yaml.Node, allowedVars map[string]struct{}, allowedFuncs map[string]struct{}) bool {
|
|
val := node.Value
|
|
for {
|
|
i := strings.Index(val, "${{")
|
|
if i == -1 {
|
|
break
|
|
}
|
|
val = val[i+3:]
|
|
j := exprEnd(val)
|
|
exprNode, parseErr := exprparser.Parse(val[:j])
|
|
if parseErr != nil {
|
|
return false
|
|
}
|
|
val = val[j+2:]
|
|
// walk expression tree
|
|
exprparser.VisitNode(exprNode, func(n exprparser.Node) {
|
|
switch el := n.(type) {
|
|
case *exprparser.FunctionNode:
|
|
if _, ok := allowedFuncs[el.Name]; !ok {
|
|
// missing function
|
|
// use a panic to break out
|
|
panic("missing function")
|
|
}
|
|
case *exprparser.ValueNode:
|
|
if el.Kind == exprparser.TokenKindNamedValue {
|
|
if _, ok := allowedVars[el.Value.(string)]; !ok {
|
|
panic("missing variable")
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
return true
|
|
}
|