github.com/nektos/act@v0.2.83/pkg/schema/schema.go (about)

     1  package schema
     2  
     3  import (
     4  	_ "embed"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"math"
     9  	"regexp"
    10  	"strconv"
    11  	"strings"
    12  
    13  	"github.com/rhysd/actionlint"
    14  	"gopkg.in/yaml.v3"
    15  )
    16  
    17  //go:embed workflow_schema.json
    18  var workflowSchema string
    19  
    20  //go:embed action_schema.json
    21  var actionSchema string
    22  
    23  var functions = regexp.MustCompile(`^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$`)
    24  
    25  type Schema struct {
    26  	Definitions map[string]Definition
    27  }
    28  
    29  func (s *Schema) GetDefinition(name string) Definition {
    30  	def, ok := s.Definitions[name]
    31  	if !ok {
    32  		switch name {
    33  		case "any":
    34  			return Definition{OneOf: &[]string{"sequence", "mapping", "number", "boolean", "string", "null"}}
    35  		case "sequence":
    36  			return Definition{Sequence: &SequenceDefinition{ItemType: "any"}}
    37  		case "mapping":
    38  			return Definition{Mapping: &MappingDefinition{LooseKeyType: "any", LooseValueType: "any"}}
    39  		case "number":
    40  			return Definition{Number: &NumberDefinition{}}
    41  		case "string":
    42  			return Definition{String: &StringDefinition{}}
    43  		case "boolean":
    44  			return Definition{Boolean: &BooleanDefinition{}}
    45  		case "null":
    46  			return Definition{Null: &NullDefinition{}}
    47  		}
    48  	}
    49  	return def
    50  }
    51  
    52  type Definition struct {
    53  	Context       []string
    54  	Mapping       *MappingDefinition
    55  	Sequence      *SequenceDefinition
    56  	OneOf         *[]string `json:"one-of"`
    57  	AllowedValues *[]string `json:"allowed-values"`
    58  	String        *StringDefinition
    59  	Number        *NumberDefinition
    60  	Boolean       *BooleanDefinition
    61  	Null          *NullDefinition
    62  }
    63  
    64  type MappingDefinition struct {
    65  	Properties     map[string]MappingProperty
    66  	LooseKeyType   string `json:"loose-key-type"`
    67  	LooseValueType string `json:"loose-value-type"`
    68  }
    69  
    70  type MappingProperty struct {
    71  	Type     string
    72  	Required bool
    73  }
    74  
    75  func (s *MappingProperty) UnmarshalJSON(data []byte) error {
    76  	if json.Unmarshal(data, &s.Type) != nil {
    77  		type MProp MappingProperty
    78  		return json.Unmarshal(data, (*MProp)(s))
    79  	}
    80  	return nil
    81  }
    82  
    83  type SequenceDefinition struct {
    84  	ItemType string `json:"item-type"`
    85  }
    86  
    87  type StringDefinition struct {
    88  	Constant     string
    89  	IsExpression bool `json:"is-expression"`
    90  }
    91  
    92  type NumberDefinition struct {
    93  }
    94  
    95  type BooleanDefinition struct {
    96  }
    97  
    98  type NullDefinition struct {
    99  }
   100  
   101  func GetWorkflowSchema() *Schema {
   102  	sh := &Schema{}
   103  	_ = json.Unmarshal([]byte(workflowSchema), sh)
   104  	return sh
   105  }
   106  
   107  func GetActionSchema() *Schema {
   108  	sh := &Schema{}
   109  	_ = json.Unmarshal([]byte(actionSchema), sh)
   110  	return sh
   111  }
   112  
   113  type Node struct {
   114  	Definition string
   115  	Schema     *Schema
   116  	Context    []string
   117  }
   118  
   119  type FunctionInfo struct {
   120  	name string
   121  	min  int
   122  	max  int
   123  }
   124  
   125  func (s *Node) checkSingleExpression(exprNode actionlint.ExprNode) error {
   126  	if len(s.Context) == 0 {
   127  		switch exprNode.Token().Kind {
   128  		case actionlint.TokenKindInt:
   129  		case actionlint.TokenKindFloat:
   130  		case actionlint.TokenKindString:
   131  			return nil
   132  		default:
   133  			return fmt.Errorf("expressions are not allowed here")
   134  		}
   135  	}
   136  
   137  	funcs := s.GetFunctions()
   138  
   139  	var err error
   140  	actionlint.VisitExprNode(exprNode, func(node, _ actionlint.ExprNode, entering bool) {
   141  		if funcCallNode, ok := node.(*actionlint.FuncCallNode); entering && ok {
   142  			for _, v := range *funcs {
   143  				if strings.EqualFold(funcCallNode.Callee, v.name) {
   144  					if v.min > len(funcCallNode.Args) {
   145  						err = errors.Join(err, fmt.Errorf("Missing parameters for %s expected >= %v got %v", funcCallNode.Callee, v.min, len(funcCallNode.Args)))
   146  					}
   147  					if v.max < len(funcCallNode.Args) {
   148  						err = errors.Join(err, fmt.Errorf("Too many parameters for %s expected <= %v got %v", funcCallNode.Callee, v.max, len(funcCallNode.Args)))
   149  					}
   150  					return
   151  				}
   152  			}
   153  			err = errors.Join(err, fmt.Errorf("Unknown Function Call %s", funcCallNode.Callee))
   154  		}
   155  		if varNode, ok := node.(*actionlint.VariableNode); entering && ok {
   156  			for _, v := range s.Context {
   157  				if strings.EqualFold(varNode.Name, v) {
   158  					return
   159  				}
   160  			}
   161  			err = errors.Join(err, fmt.Errorf("Unknown Variable Access %s", varNode.Name))
   162  		}
   163  	})
   164  	return err
   165  }
   166  
   167  func (s *Node) GetFunctions() *[]FunctionInfo {
   168  	funcs := &[]FunctionInfo{}
   169  	AddFunction(funcs, "contains", 2, 2)
   170  	AddFunction(funcs, "endsWith", 2, 2)
   171  	AddFunction(funcs, "format", 1, 255)
   172  	AddFunction(funcs, "join", 1, 2)
   173  	AddFunction(funcs, "startsWith", 2, 2)
   174  	AddFunction(funcs, "toJson", 1, 1)
   175  	AddFunction(funcs, "fromJson", 1, 1)
   176  	for _, v := range s.Context {
   177  		i := strings.Index(v, "(")
   178  		if i == -1 {
   179  			continue
   180  		}
   181  		smatch := functions.FindStringSubmatch(v)
   182  		if len(smatch) > 0 {
   183  			functionName := smatch[1]
   184  			minParameters, _ := strconv.ParseInt(smatch[2], 10, 32)
   185  			maxParametersRaw := smatch[3]
   186  			var maxParameters int64
   187  			if strings.EqualFold(maxParametersRaw, "MAX") {
   188  				maxParameters = math.MaxInt32
   189  			} else {
   190  				maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
   191  			}
   192  			*funcs = append(*funcs, FunctionInfo{
   193  				name: functionName,
   194  				min:  int(minParameters),
   195  				max:  int(maxParameters),
   196  			})
   197  		}
   198  	}
   199  	return funcs
   200  }
   201  
   202  func (s *Node) checkExpression(node *yaml.Node) (bool, error) {
   203  	val := node.Value
   204  	hadExpr := false
   205  	var err error
   206  	for {
   207  		if i := strings.Index(val, "${{"); i != -1 {
   208  			val = val[i+3:]
   209  		} else {
   210  			return hadExpr, err
   211  		}
   212  		hadExpr = true
   213  
   214  		parser := actionlint.NewExprParser()
   215  		lexer := actionlint.NewExprLexer(val)
   216  		exprNode, parseErr := parser.Parse(lexer)
   217  		if parseErr != nil {
   218  			err = errors.Join(err, fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message))
   219  			continue
   220  		}
   221  		val = val[lexer.Offset():]
   222  		cerr := s.checkSingleExpression(exprNode)
   223  		if cerr != nil {
   224  			err = errors.Join(err, fmt.Errorf("%s%w", formatLocation(node), cerr))
   225  		}
   226  	}
   227  }
   228  
   229  func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
   230  	*funcs = append(*funcs, FunctionInfo{
   231  		name: s,
   232  		min:  i1,
   233  		max:  i2,
   234  	})
   235  }
   236  
   237  func (s *Node) UnmarshalYAML(node *yaml.Node) error {
   238  	if node != nil && node.Kind == yaml.DocumentNode {
   239  		return s.UnmarshalYAML(node.Content[0])
   240  	}
   241  	def := s.Schema.GetDefinition(s.Definition)
   242  	if s.Context == nil {
   243  		s.Context = def.Context
   244  	}
   245  
   246  	isExpr, err := s.checkExpression(node)
   247  	if err != nil {
   248  		return err
   249  	}
   250  	if isExpr {
   251  		return nil
   252  	}
   253  	if def.Mapping != nil {
   254  		return s.checkMapping(node, def)
   255  	} else if def.Sequence != nil {
   256  		return s.checkSequence(node, def)
   257  	} else if def.OneOf != nil {
   258  		return s.checkOneOf(def, node)
   259  	}
   260  
   261  	if node.Kind != yaml.ScalarNode {
   262  		return fmt.Errorf("%sExpected a scalar got %v", formatLocation(node), getStringKind(node.Kind))
   263  	}
   264  
   265  	if def.String != nil {
   266  		return s.checkString(node, def)
   267  	} else if def.Number != nil {
   268  		var num float64
   269  		return node.Decode(&num)
   270  	} else if def.Boolean != nil {
   271  		var b bool
   272  		return node.Decode(&b)
   273  	} else if def.AllowedValues != nil {
   274  		s := node.Value
   275  		for _, v := range *def.AllowedValues {
   276  			if s == v {
   277  				return nil
   278  			}
   279  		}
   280  		return fmt.Errorf("%sExpected one of %s got %s", formatLocation(node), strings.Join(*def.AllowedValues, ","), s)
   281  	} else if def.Null != nil {
   282  		var myNull *byte
   283  		return node.Decode(&myNull)
   284  	}
   285  	return errors.ErrUnsupported
   286  }
   287  
   288  func (s *Node) checkString(node *yaml.Node, def Definition) error {
   289  	val := node.Value
   290  	if def.String.Constant != "" && def.String.Constant != val {
   291  		return fmt.Errorf("%sExpected %s got %s", formatLocation(node), def.String.Constant, val)
   292  	}
   293  	if def.String.IsExpression {
   294  		parser := actionlint.NewExprParser()
   295  		lexer := actionlint.NewExprLexer(val + "}}")
   296  		exprNode, parseErr := parser.Parse(lexer)
   297  		if parseErr != nil {
   298  			return fmt.Errorf("%sFailed to parse: %s", formatLocation(node), parseErr.Message)
   299  		}
   300  		cerr := s.checkSingleExpression(exprNode)
   301  		if cerr != nil {
   302  			return fmt.Errorf("%s%w", formatLocation(node), cerr)
   303  		}
   304  	}
   305  	return nil
   306  }
   307  
   308  func (s *Node) checkOneOf(def Definition, node *yaml.Node) error {
   309  	var allErrors error
   310  	for _, v := range *def.OneOf {
   311  		sub := &Node{
   312  			Definition: v,
   313  			Schema:     s.Schema,
   314  			Context:    append(append([]string{}, s.Context...), s.Schema.GetDefinition(v).Context...),
   315  		}
   316  
   317  		err := sub.UnmarshalYAML(node)
   318  		if err == nil {
   319  			return nil
   320  		}
   321  		allErrors = errors.Join(allErrors, fmt.Errorf("%sFailed to match %s: %w", formatLocation(node), v, err))
   322  	}
   323  	return allErrors
   324  }
   325  
   326  func getStringKind(k yaml.Kind) string {
   327  	switch k {
   328  	case yaml.DocumentNode:
   329  		return "document"
   330  	case yaml.SequenceNode:
   331  		return "sequence"
   332  	case yaml.MappingNode:
   333  		return "mapping"
   334  	case yaml.ScalarNode:
   335  		return "scalar"
   336  	case yaml.AliasNode:
   337  		return "alias"
   338  	default:
   339  		return "unknown"
   340  	}
   341  }
   342  
   343  func (s *Node) checkSequence(node *yaml.Node, def Definition) error {
   344  	if node.Kind != yaml.SequenceNode {
   345  		return fmt.Errorf("%sExpected a sequence got %v", formatLocation(node), getStringKind(node.Kind))
   346  	}
   347  	var allErrors error
   348  	for _, v := range node.Content {
   349  		allErrors = errors.Join(allErrors, (&Node{
   350  			Definition: def.Sequence.ItemType,
   351  			Schema:     s.Schema,
   352  			Context:    append(append([]string{}, s.Context...), s.Schema.GetDefinition(def.Sequence.ItemType).Context...),
   353  		}).UnmarshalYAML(v))
   354  	}
   355  	return allErrors
   356  }
   357  
   358  func formatLocation(node *yaml.Node) string {
   359  	return fmt.Sprintf("Line: %v Column %v: ", node.Line, node.Column)
   360  }
   361  
   362  func (s *Node) checkMapping(node *yaml.Node, def Definition) error {
   363  	if node.Kind != yaml.MappingNode {
   364  		return fmt.Errorf("%sExpected a mapping got %v", formatLocation(node), getStringKind(node.Kind))
   365  	}
   366  	insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`)
   367  	var allErrors error
   368  	for i, k := range node.Content {
   369  		if i%2 == 0 {
   370  			if insertDirective.MatchString(k.Value) {
   371  				if len(s.Context) == 0 {
   372  					allErrors = errors.Join(allErrors, fmt.Errorf("%sinsert is not allowed here", formatLocation(k)))
   373  				}
   374  				continue
   375  			}
   376  
   377  			isExpr, err := s.checkExpression(k)
   378  			if err != nil {
   379  				allErrors = errors.Join(allErrors, err)
   380  				continue
   381  			}
   382  			if isExpr {
   383  				continue
   384  			}
   385  			vdef, ok := def.Mapping.Properties[k.Value]
   386  			if !ok {
   387  				if def.Mapping.LooseValueType == "" {
   388  					allErrors = errors.Join(allErrors, fmt.Errorf("%sUnknown Property %v", formatLocation(k), k.Value))
   389  					continue
   390  				}
   391  				vdef = MappingProperty{Type: def.Mapping.LooseValueType}
   392  			}
   393  
   394  			if err := (&Node{
   395  				Definition: vdef.Type,
   396  				Schema:     s.Schema,
   397  				Context:    append(append([]string{}, s.Context...), s.Schema.GetDefinition(vdef.Type).Context...),
   398  			}).UnmarshalYAML(node.Content[i+1]); err != nil {
   399  				allErrors = errors.Join(allErrors, err)
   400  				continue
   401  			}
   402  		}
   403  	}
   404  	return allErrors
   405  }