This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package jobparser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"gitea.com/gitea/runner/act/model"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
// maxExpandedNodes bounds how many nodes alias expansion may create. go-yaml's own alias guard does
|
||||
// not cover us: it only counts while decoding into values, and a workflow is kept as raw yaml.Nodes.
|
||||
const maxExpandedNodes = 50000
|
||||
|
||||
var errTooManyYamlNodes = errors.New("maximum YAML nodes exceeded")
|
||||
|
||||
// ReadWorkflow decodes a workflow file with its aliases expanded. Callers inspect the workflow's
|
||||
// raw nodes by kind, and an alias is a kind none of them expect.
|
||||
func ReadWorkflow(content []byte) (*model.Workflow, error) {
|
||||
doc, err := resolveYamlAliases(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return readWorkflowDoc(doc)
|
||||
}
|
||||
|
||||
func readWorkflowDoc(doc *yaml.Node) (*model.Workflow, error) {
|
||||
if doc.Kind == 0 {
|
||||
return nil, io.EOF // what a yaml decoder reports for an empty file
|
||||
}
|
||||
w := new(model.Workflow)
|
||||
return w, doc.Decode(w)
|
||||
}
|
||||
|
||||
// decodeResolved is yaml.Unmarshal with aliases expanded first.
|
||||
func decodeResolved(content []byte, out any) error {
|
||||
doc, err := resolveYamlAliases(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return decodeYamlDoc(doc, out)
|
||||
}
|
||||
|
||||
func decodeYamlDoc(doc *yaml.Node, out any) error {
|
||||
if doc.Kind == 0 {
|
||||
return nil // an empty document, as yaml.Unmarshal treats it
|
||||
}
|
||||
return doc.Decode(out)
|
||||
}
|
||||
|
||||
// resolveYamlAliases parses content and replaces every alias with a copy of the node its anchor names.
|
||||
func resolveYamlAliases(content []byte) (*yaml.Node, error) {
|
||||
doc := &yaml.Node{}
|
||||
if err := yaml.Unmarshal(content, doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
budget := maxExpandedNodes
|
||||
return doc, expandAliases(doc, &budget)
|
||||
}
|
||||
|
||||
// expandAliases replaces node's alias descendants in place.
|
||||
func expandAliases(node *yaml.Node, budget *int) error {
|
||||
node.Anchor = "" // a name for a node, not part of the workflow: keep it out of the payloads
|
||||
if err := rejectMergeKeys(node); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, child := range node.Content {
|
||||
if child.Kind != yaml.AliasNode {
|
||||
if err := expandAliases(child, budget); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
copied, err := copyExpanded(child.Alias, budget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node.Content[i] = copied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyExpanded deep copies a node expandAliases already expanded and validated, since an anchor is
|
||||
// declared before the alias naming it. An anchor aliased from inside itself is the exception, and
|
||||
// recurses here until it exhausts budget.
|
||||
func copyExpanded(node *yaml.Node, budget *int) (*yaml.Node, error) {
|
||||
if *budget--; *budget < 0 {
|
||||
return nil, errTooManyYamlNodes
|
||||
}
|
||||
if node.Kind == yaml.AliasNode {
|
||||
return copyExpanded(node.Alias, budget)
|
||||
}
|
||||
|
||||
copied := *node
|
||||
copied.Content = make([]*yaml.Node, len(node.Content))
|
||||
for i, child := range node.Content {
|
||||
child, err := copyExpanded(child, budget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copied.Content[i] = child
|
||||
}
|
||||
return &copied, nil
|
||||
}
|
||||
|
||||
// rejectMergeKeys refuses `<<: *anchor`, same as GitHub does
|
||||
func rejectMergeKeys(node *yaml.Node) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(node.Content)-1; i += 2 {
|
||||
if node.Content[i].Tag == "!!merge" {
|
||||
return errors.New("merge keys (`<<`) are not supported, alias the whole value instead")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user