github.com/nektos/act@v0.2.63/pkg/exprparser/functions.go (about) 1 package exprparser 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "io" 9 "io/fs" 10 "os" 11 "path/filepath" 12 "reflect" 13 "strconv" 14 "strings" 15 16 "github.com/go-git/go-git/v5/plumbing/format/gitignore" 17 18 "github.com/nektos/act/pkg/model" 19 "github.com/rhysd/actionlint" 20 ) 21 22 func (impl *interperterImpl) contains(search, item reflect.Value) (bool, error) { 23 switch search.Kind() { 24 case reflect.String, reflect.Int, reflect.Float64, reflect.Bool, reflect.Invalid: 25 return strings.Contains( 26 strings.ToLower(impl.coerceToString(search).String()), 27 strings.ToLower(impl.coerceToString(item).String()), 28 ), nil 29 30 case reflect.Slice: 31 for i := 0; i < search.Len(); i++ { 32 arrayItem := search.Index(i).Elem() 33 result, err := impl.compareValues(arrayItem, item, actionlint.CompareOpNodeKindEq) 34 if err != nil { 35 return false, err 36 } 37 38 if isEqual, ok := result.(bool); ok && isEqual { 39 return true, nil 40 } 41 } 42 } 43 44 return false, nil 45 } 46 47 func (impl *interperterImpl) startsWith(searchString, searchValue reflect.Value) (bool, error) { 48 return strings.HasPrefix( 49 strings.ToLower(impl.coerceToString(searchString).String()), 50 strings.ToLower(impl.coerceToString(searchValue).String()), 51 ), nil 52 } 53 54 func (impl *interperterImpl) endsWith(searchString, searchValue reflect.Value) (bool, error) { 55 return strings.HasSuffix( 56 strings.ToLower(impl.coerceToString(searchString).String()), 57 strings.ToLower(impl.coerceToString(searchValue).String()), 58 ), nil 59 } 60 61 const ( 62 passThrough = iota 63 bracketOpen 64 bracketClose 65 ) 66 67 func (impl *interperterImpl) format(str reflect.Value, replaceValue ...reflect.Value) (string, error) { 68 input := impl.coerceToString(str).String() 69 output := "" 70 replacementIndex := "" 71 72 state := passThrough 73 for _, character := range input { 74 switch state { 75 case passThrough: // normal buffer output 76 switch character { 77 case '{': 78 state = bracketOpen 79 80 case '}': 81 state = bracketClose 82 83 default: 84 output += string(character) 85 } 86 87 case bracketOpen: // found { 88 switch character { 89 case '{': 90 output += "{" 91 replacementIndex = "" 92 state = passThrough 93 94 case '}': 95 index, err := strconv.ParseInt(replacementIndex, 10, 32) 96 if err != nil { 97 return "", fmt.Errorf("The following format string is invalid: '%s'", input) 98 } 99 100 replacementIndex = "" 101 102 if len(replaceValue) <= int(index) { 103 return "", fmt.Errorf("The following format string references more arguments than were supplied: '%s'", input) 104 } 105 106 output += impl.coerceToString(replaceValue[index]).String() 107 108 state = passThrough 109 110 default: 111 replacementIndex += string(character) 112 } 113 114 case bracketClose: // found } 115 switch character { 116 case '}': 117 output += "}" 118 replacementIndex = "" 119 state = passThrough 120 121 default: 122 panic("Invalid format parser state") 123 } 124 } 125 } 126 127 if state != passThrough { 128 switch state { 129 case bracketOpen: 130 return "", fmt.Errorf("Unclosed brackets. The following format string is invalid: '%s'", input) 131 132 case bracketClose: 133 return "", fmt.Errorf("Closing bracket without opening one. The following format string is invalid: '%s'", input) 134 } 135 } 136 137 return output, nil 138 } 139 140 func (impl *interperterImpl) join(array reflect.Value, sep reflect.Value) (string, error) { 141 separator := impl.coerceToString(sep).String() 142 switch array.Kind() { 143 case reflect.Slice: 144 var items []string 145 for i := 0; i < array.Len(); i++ { 146 items = append(items, impl.coerceToString(array.Index(i).Elem()).String()) 147 } 148 149 return strings.Join(items, separator), nil 150 default: 151 return strings.Join([]string{impl.coerceToString(array).String()}, separator), nil 152 } 153 } 154 155 func (impl *interperterImpl) toJSON(value reflect.Value) (string, error) { 156 if value.Kind() == reflect.Invalid { 157 return "null", nil 158 } 159 160 json, err := json.MarshalIndent(value.Interface(), "", " ") 161 if err != nil { 162 return "", fmt.Errorf("Cannot convert value to JSON. Cause: %v", err) 163 } 164 165 return string(json), nil 166 } 167 168 func (impl *interperterImpl) fromJSON(value reflect.Value) (interface{}, error) { 169 if value.Kind() != reflect.String { 170 return nil, fmt.Errorf("Cannot parse non-string type %v as JSON", value.Kind()) 171 } 172 173 var data interface{} 174 175 err := json.Unmarshal([]byte(value.String()), &data) 176 if err != nil { 177 return nil, fmt.Errorf("Invalid JSON: %v", err) 178 } 179 180 return data, nil 181 } 182 183 func (impl *interperterImpl) hashFiles(paths ...reflect.Value) (string, error) { 184 var ps []gitignore.Pattern 185 186 const cwdPrefix = "." + string(filepath.Separator) 187 const excludeCwdPrefix = "!" + cwdPrefix 188 for _, path := range paths { 189 if path.Kind() == reflect.String { 190 cleanPath := path.String() 191 if strings.HasPrefix(cleanPath, cwdPrefix) { 192 cleanPath = cleanPath[len(cwdPrefix):] 193 } else if strings.HasPrefix(cleanPath, excludeCwdPrefix) { 194 cleanPath = "!" + cleanPath[len(excludeCwdPrefix):] 195 } 196 ps = append(ps, gitignore.ParsePattern(cleanPath, nil)) 197 } else { 198 return "", fmt.Errorf("Non-string path passed to hashFiles") 199 } 200 } 201 202 matcher := gitignore.NewMatcher(ps) 203 204 var files []string 205 if err := filepath.Walk(impl.config.WorkingDir, func(path string, fi fs.FileInfo, err error) error { 206 if err != nil { 207 return err 208 } 209 sansPrefix := strings.TrimPrefix(path, impl.config.WorkingDir+string(filepath.Separator)) 210 parts := strings.Split(sansPrefix, string(filepath.Separator)) 211 if fi.IsDir() || !matcher.Match(parts, fi.IsDir()) { 212 return nil 213 } 214 files = append(files, path) 215 return nil 216 }); err != nil { 217 return "", fmt.Errorf("Unable to filepath.Walk: %v", err) 218 } 219 220 if len(files) == 0 { 221 return "", nil 222 } 223 224 hasher := sha256.New() 225 226 for _, file := range files { 227 f, err := os.Open(file) 228 if err != nil { 229 return "", fmt.Errorf("Unable to os.Open: %v", err) 230 } 231 232 if _, err := io.Copy(hasher, f); err != nil { 233 return "", fmt.Errorf("Unable to io.Copy: %v", err) 234 } 235 236 if err := f.Close(); err != nil { 237 return "", fmt.Errorf("Unable to Close file: %v", err) 238 } 239 } 240 241 return hex.EncodeToString(hasher.Sum(nil)), nil 242 } 243 244 func (impl *interperterImpl) getNeedsTransitive(job *model.Job) []string { 245 needs := job.Needs() 246 247 for _, need := range needs { 248 parentNeeds := impl.getNeedsTransitive(impl.config.Run.Workflow.GetJob(need)) 249 needs = append(needs, parentNeeds...) 250 } 251 252 return needs 253 } 254 255 func (impl *interperterImpl) always() (bool, error) { 256 return true, nil 257 } 258 259 func (impl *interperterImpl) jobSuccess() (bool, error) { 260 jobs := impl.config.Run.Workflow.Jobs 261 jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) 262 263 for _, needs := range jobNeeds { 264 if jobs[needs].Result != "success" { 265 return false, nil 266 } 267 } 268 269 return true, nil 270 } 271 272 func (impl *interperterImpl) stepSuccess() (bool, error) { 273 return impl.env.Job.Status == "success", nil 274 } 275 276 func (impl *interperterImpl) jobFailure() (bool, error) { 277 jobs := impl.config.Run.Workflow.Jobs 278 jobNeeds := impl.getNeedsTransitive(impl.config.Run.Job()) 279 280 for _, needs := range jobNeeds { 281 if jobs[needs].Result == "failure" { 282 return true, nil 283 } 284 } 285 286 return false, nil 287 } 288 289 func (impl *interperterImpl) stepFailure() (bool, error) { 290 return impl.env.Job.Status == "failure", nil 291 } 292 293 func (impl *interperterImpl) cancelled() (bool, error) { 294 return impl.env.Job.Status == "cancelled", nil 295 }