1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6 "math"
7 "regexp"
8 "strconv"
9 "strings"
10)
11
12// Cron validation, ported from converter/cron.ts.
13
14type cronRange struct {
15 min, max int
16 names map[string]int
17}
18
19var (
20 cronMinute = cronRange{0, 59, nil}
21 cronHour = cronRange{0, 23, nil}
22 cronDOM = cronRange{1, 31, nil}
23 cronMonth = cronRange{1, 12, map[string]int{"jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12}}
24 cronDOW = cronRange{0, 6, map[string]int{"sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6}}
25)
26
27func isValidCron(cron string) bool {
28 parts := splitSpaces(cron)
29 if len(parts) != 5 {
30 return false
31 }
32 return validateCronPart(parts[0], cronMinute, true) && validateCronPart(parts[1], cronHour, true) &&
33 validateCronPart(parts[2], cronDOM, true) && validateCronPart(parts[3], cronMonth, true) &&
34 validateCronPart(parts[4], cronDOW, true)
35}
36
37// splitSpaces is JavaScript's split(/ +/): runs of spaces separate, other
38// whitespace does not, and leading or trailing spaces give empty parts.
39func splitSpaces(s string) []string {
40 var parts []string
41 start := 0
42 i := 0
43 for i < len(s) {
44 if s[i] != ' ' {
45 i++
46 continue
47 }
48 parts = append(parts, s[start:i])
49 for i < len(s) && s[i] == ' ' {
50 i++
51 }
52 start = i
53 }
54 return append(parts, s[start:])
55}
56
57var jsDecimal = regexp.MustCompile(`^[+-]?(?:[0-9]+\.?[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$`)
58
59// jsNumber is JavaScript's Number(string): surrounding white space is
60// ignored, an empty string is 0, and decimal, hexadecimal, octal and binary
61// literals and Infinity are numbers. ok is false for NaN.
62func jsNumber(v string) (float64, bool) {
63 t := strings.Trim(v, " \t\n\v\f\r\u00a0\u2028\u2029\ufeff")
64 switch {
65 case t == "":
66 return 0, true
67 case jsDecimal.MatchString(t):
68 f, err := strconv.ParseFloat(t, 64)
69 return f, err == nil || math.IsInf(f, 0)
70 case t == "Infinity" || t == "+Infinity":
71 return math.Inf(1), true
72 case t == "-Infinity":
73 return math.Inf(-1), true
74 case len(t) > 2 && t[0] == '0':
75 base := map[byte]int{'x': 16, 'X': 16, 'o': 8, 'O': 8, 'b': 2, 'B': 2}[t[1]]
76 if base == 0 {
77 return 0, false
78 }
79 n, err := strconv.ParseUint(t[2:], base, 64)
80 if err != nil {
81 if ne, ok := err.(*strconv.NumError); ok && ne.Err == strconv.ErrRange {
82 return math.Inf(1), true
83 }
84 return 0, false
85 }
86 return float64(n), true
87 }
88 return 0, false
89}
90
91func cronNumber(v string) (float64, bool) { return jsNumber(v) }
92
93func validateCronPart(value string, r cronRange, allowSeparators bool) bool {
94 lower := strings.ToLower(value)
95 if _, ok := r.names[lower]; ok {
96 return true
97 }
98 if value == "*" {
99 return true
100 }
101 if strings.Contains(value, ",") {
102 if !allowSeparators {
103 return false
104 }
105 for _, v := range strings.Split(value, ",") {
106 if v == "" || !validateCronPart(v, r, true) {
107 return false
108 }
109 }
110 return true
111 }
112 if strings.Contains(value, "/") {
113 if !allowSeparators {
114 return false
115 }
116 parts := strings.Split(value, "/")
117 start, step := parts[0], ""
118 if len(parts) > 1 {
119 step = parts[1]
120 }
121 n, ok := cronNumber(step)
122 if len(parts) > 2 || !ok || n <= 0 || start == "" || step == "" {
123 return false
124 }
125 return validateCronPart(start, r, true) && validateCronPart(step, r, false)
126 }
127 if strings.Contains(value, "-") {
128 if !allowSeparators {
129 return false
130 }
131 parts := strings.Split(value, "-")
132 if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
133 return false
134 }
135 s, e := cronRangeValue(parts[0], r), cronRangeValue(parts[1], r)
136 return validateCronPart(parts[0], r, false) && validateCronPart(parts[1], r, false) && e >= s
137 }
138 n, ok := cronNumber(value)
139 return ok && n >= float64(r.min) && n <= float64(r.max)
140}
141
142func cronRangeValue(v string, r cronRange) float64 {
143 if n, ok := r.names[strings.ToLower(v)]; ok {
144 return float64(n)
145 }
146 n, ok := cronNumber(v)
147 if !ok {
148 return math.NaN()
149 }
150 return n
151}