diff options
Diffstat (limited to 'pkg/exprparser/functions.go')
-rw-r--r-- | pkg/exprparser/functions.go | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/pkg/exprparser/functions.go b/pkg/exprparser/functions.go new file mode 100644 index 0000000..83b2a08 --- /dev/null +++ b/pkg/exprparser/functions.go @@ -0,0 +1,295 @@ +package exprparser + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" + + "github.com/nektos/act/pkg/model" + "github.com/rhysd/actionlint" +) + +func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) { + switch search.Kind() { + case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid: + return strings.Contains( + strings.ToLower(impl.coerceToString(search).String()), + strings.ToLower(impl.coerceToString(item).String()), + ), nil + + case reflect.Slice: + for i := 0; i < search.Len(); i++ { + arrayItem := search.Index(i).Elem() + result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq) + if err != nil { + return false, err + } + + if isEqual, ok := result.(bool); ok && isEqual { + return true, nil + } + } + } + + return false, nil +} + +func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) { + return strings.HasPrefix( + strings.ToLower(impl.coerceToString(searchString).String()), + strings.ToLower(impl.coerceToString(searchValue).String()), + ), nil +} + +func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) { + return strings.HasSuffix( + strings.ToLower(impl.coerceToString(searchString).String()), + strings.ToLower(impl.coerceToString(searchValue).String()), + ), nil +} + +const ( + passThrough = iota + bracketOpen + bracketClose +) + +func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) { + input := impl.coerceToString(str).String() + output := "" + replacementIndex := "" + + state := passThrough + for _, character := range input { + switch state { + case passThrough: // normal buffer output + switch character { + case '{': + state = bracketOpen + + case '}': + state = bracketClose + + default: + output += string(character) + } + + case bracketOpen: // found { + switch character { + case '{': + output += "{" + replacementIndex = "" + state = passThrough + + case '}': + index, err := strconv.ParseInt(replacementIndex, 10, 32) + if err != nil { + return "", fmt.Errorf("The following format string is invalid: '%s'", input) + } + + replacementIndex = "" + + if len(replaceValue) <= int(index) { + return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input) + } + + output += impl.coerceToString(replaceValue[index]).String() + + state = passThrough + + default: + replacementIndex += string(character) + } + + case bracketClose: // found } + switch character { + case '}': + output += "}" + replacementIndex = "" + state = passThrough + + default: + panic("Invalid format parser state") + } + } + } + + if state != passThrough { + switch state { + case bracketOpen: + return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input) + + case bracketClose: + return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input) + } + } + + return output, nil +} + +func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) { + separator := impl.coerceToString(sep).String() + switch array.Kind() { + case reflect.Slice: + var items []string + for i := 0; i < array.Len(); i++ { + items = append(items, impl.coerceToString(array.Index(i).Elem()).String()) + } + + return strings.Join(items, separator), nil + default: + return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil + } +} + +func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) { + if value.Kind() == reflect.Invalid { + return "null", nil + } + + json, err := json.MarshalIndent(value.Interface(), "", " ") + if err != nil { + return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err) + } + + return string(json), nil +} + +func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error) { + if value.Kind() != reflect.String { + return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind()) + } + + var data interface{} + + err := json.Unmarshal([]byte(value.String()), &data) + if err != nil { + return nil, fmt.Errorf("Invalid JSON: %v", err) + } + + return data, nil +} + +func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) { + var ps []gitignore.Pattern + + const cwdPrefix = "." + string(filepath.Separator) + const excludeCwdPrefix = "!" + cwdPrefix + for _, path := range paths { + if path.Kind() == reflect.String { + cleanPath := path.String() + if strings.HasPrefix(cleanPath, cwdPrefix) { + cleanPath = cleanPath[len(cwdPrefix):] + } else if strings.HasPrefix(cleanPath, excludeCwdPrefix) { + cleanPath = "!" + cleanPath[len(excludeCwdPrefix):] + } + ps = append(ps, gitignore.ParsePattern(cleanPath, nil)) + } else { + return "", fmt.Errorf("Non-string path passed to hashFiles") + } + } + + matcher := gitignore.NewMatcher(ps) + + var files []string + if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error { + if err != nil { + return err + } + sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator)) + parts := strings.Split(sansPrefix, string(filepath.Separator)) + if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) { + return nil + } + files = append(files, path) + return nil + }); err != nil { + return "", fmt.Errorf("Unable to filepath.Walk: %v", err) + } + + if len(files) == 0 { + return "", nil + } + + hasher := sha256.New() + + for _, file := range files { + f, err := os.Open(file) + if err != nil { + return "", fmt.Errorf("Unable to os.Open: %v", err) + } + + if _, err := io.Copy(hasher, f); err != nil { + return "", fmt.Errorf("Unable to io.Copy: %v", err) + } + + if err := f.Close(); err != nil { + return "", fmt.Errorf("Unable to Close file: %v", err) + } + } + + return hex.EncodeToString(hasher.Sum(nil)), nil +} + +func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string { + needs := job.Needs() + + for _, need := range needs { + parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need)) + needs = append(needs, parentNeeds...) + } + + return needs +} + +func (impl *interperterImpl) always() (bool, error) { + return true, nil +} + +func (impl *interperterImpl) jobSuccess() (bool, error) { + jobs := impl.config.Run.Workflow.Jobs + jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) + + for _, needs := range jobNeeds { + if jobs[needs].Result != "success" { + return false, nil + } + } + + return true, nil +} + +func (impl *interperterImpl) stepSuccess() (bool, error) { + return impl.env.Job.Status == "success", nil +} + +func (impl *interperterImpl) jobFailure() (bool, error) { + jobs := impl.config.Run.Workflow.Jobs + jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) + + for _, needs := range jobNeeds { + if jobs[needs].Result == "failure" { + return true, nil + } + } + + return false, nil +} + +func (impl *interperterImpl) stepFailure() (bool, error) { + return impl.env.Job.Status == "failure", nil +} + +func (impl *interperterImpl) cancelled() (bool, error) { + return impl.env.Job.Status == "cancelled", nil +} |