Blame SOURCES/0002-Add-random-within-range-operator.patch

11487f
From 8c3f71bbe109f5df8280eeaa2152dabc4f48474a Mon Sep 17 00:00:00 2001
11487f
From: =?UTF-8?q?Ond=C5=99ej=20Poho=C5=99elsk=C3=BD?=
11487f
 <35430604+opohorel@users.noreply.github.com>
11487f
Date: Mon, 8 Nov 2021 16:20:09 +0100
11487f
Subject: [PATCH 2/5] Add random within range '~' operator
11487f
11487f
With the operator one can specify for a job a random time or date within
11487f
a specified range for a field.
11487f
The random value is generated when the crontab where the job is
11487f
specified, is loaded.
11487f
---
11487f
 man/crontab.5 |   9 ++
11487f
 src/entry.c   | 267 +++++++++++++++++++++++++++++++-------------------
11487f
 2 files changed, 175 insertions(+), 101 deletions(-)
11487f
11487f
diff --git a/man/crontab.5 b/man/crontab.5
11487f
index 04358cb..5d89862 100644
11487f
--- a/man/crontab.5
11487f
+++ b/man/crontab.5
11487f
@@ -205,6 +205,15 @@ hyphen.  The specified range is inclusive.  For example, 8-11 for
11487f
 an 'hours' entry specifies execution at hours 8, 9, 10, and 11. The first
11487f
 number must be less than or equal to the second one.
11487f
 .PP
11487f
+Randomization of the execution time within a range can be used.
11487f
+A random number within a range specified as two numbers separated with
11487f
+a tilde is picked.  The specified range is inclusive.
11487f
+For example, 6~15 for a 'minutes' entry picks a random minute
11487f
+within 6 to 15 range.  The random number is picked when crontab file is parsed.
11487f
+The first number must be less than or equal to the second one. You might omit
11487f
+one or both of the numbers specifying the range.  For example, ~ for a 'minutes'
11487f
+entry picks a random minute within 0 to 59 range.
11487f
+.PP
11487f
 Lists are allowed.  A list is a set of numbers (or ranges) separated by
11487f
 commas.  Examples: "1,2,5,9", "0-4,8-12".
11487f
 .PP
11487f
diff --git a/src/entry.c b/src/entry.c
11487f
index 36e639e..f2bb717 100644
11487f
--- a/src/entry.c
11487f
+++ b/src/entry.c
11487f
@@ -62,9 +62,22 @@ static const char *ecodes[] = {
11487f
 	"out of memory"
11487f
 };
11487f
 
11487f
+typedef enum {
11487f
+	R_START,
11487f
+	R_AST,
11487f
+	R_STEP,
11487f
+	R_TERMS,
11487f
+	R_NUM1,
11487f
+	R_RANGE,
11487f
+	R_RANGE_NUM2,
11487f
+	R_RANDOM,
11487f
+	R_RANDOM_NUM2,
11487f
+	R_FINISH,
11487f
+} range_state_t;
11487f
+
11487f
 static int get_list(bitstr_t *, int, int, const char *[], int, FILE *),
11487f
-get_range(bitstr_t *, int, int, const char *[], int, FILE *),
11487f
-get_number(int *, int, const char *[], int, FILE *, const char *),
11487f
+get_range(bitstr_t *, int, int, const char *[], FILE *),
11487f
+get_number(int *, int, const char *[], FILE *),
11487f
 set_element(bitstr_t *, int, int, int);
11487f
 
11487f
 void free_entry(entry * e) {
11487f
@@ -467,11 +480,14 @@ get_list(bitstr_t * bits, int low, int high, const char *names[],
11487f
 	/* process all ranges
11487f
 	 */
11487f
 	done = FALSE;
11487f
+	/* unget ch to allow get_range() to process it properly 
11487f
+	 */
11487f
+	unget_char(ch, file);
11487f
 	while (!done) {
11487f
-		if (EOF == (ch = get_range(bits, low, high, names, ch, file)))
11487f
+		if (EOF == (ch = get_range(bits, low, high, names, file)))
11487f
 			return (EOF);
11487f
 		if (ch == ',')
11487f
-			ch = get_char(file);
11487f
+			continue;
11487f
 		else
11487f
 			done = TRUE;
11487f
 	}
11487f
@@ -486,144 +502,193 @@ get_list(bitstr_t * bits, int low, int high, const char *names[],
11487f
 	return (ch);
11487f
 }
11487f
 
11487f
+inline static int is_separator(int ch) {
11487f
+	switch (ch) {
11487f
+		case '\t':
11487f
+		case '\n':
11487f
+		case ' ':
11487f
+		case ',':
11487f
+			return 1;
11487f
+		default:
11487f
+			return 0;
11487f
+	}
11487f
+}
11487f
+
11487f
+
11487f
 
11487f
 static int
11487f
 get_range(bitstr_t * bits, int low, int high, const char *names[],
11487f
-	int ch, FILE * file) {
11487f
+		FILE * file) {
11487f
 	/* range = number | number "-" number [ "/" number ]
11487f
+	 *         | [number] "~" [number]
11487f
 	 */
11487f
+	
11487f
+	int ch, i, num1, num2, num3;
11487f
 
11487f
-	int i, num1, num2, num3;
11487f
+	/* default value for step
11487f
+	 */
11487f
+	num3 = 1;
11487f
+	range_state_t state = R_START;
11487f
+
11487f
+	while (state != R_FINISH && ((ch = get_char(file)) != EOF)) {
11487f
+		switch (state) {
11487f
+			case R_START:
11487f
+				if (ch == '*') {
11487f
+					num1 = low;
11487f
+					num2 = high;
11487f
+					state = R_AST;
11487f
+					break;
11487f
+				}
11487f
+				if (ch == '~') {
11487f
+					num1 = low;
11487f
+					state = R_RANDOM;
11487f
+					break;
11487f
+				}
11487f
+				unget_char(ch, file);
11487f
+				if (get_number(&num1, low, names, file) != EOF) {
11487f
+					state = R_NUM1;
11487f
+					break;
11487f
+				}
11487f
+				return (EOF);
11487f
 
11487f
-	Debug(DPARS | DEXT, ("get_range()...entering, exit won't show\n"));
11487f
+			case R_AST:
11487f
+				if (ch == '/') {
11487f
+					state = R_STEP;
11487f
+					break;
11487f
+				}
11487f
+				if (is_separator(ch)) {
11487f
+					state = R_FINISH;
11487f
+					break;
11487f
+				}
11487f
+				return (EOF);
11487f
 
11487f
-	if (ch == '*') {
11487f
-		/* '*' means "first-last" but can still be modified by /step
11487f
-		 */
11487f
-		num1 = low;
11487f
-		num2 = high;
11487f
-		ch = get_char(file);
11487f
-		if (ch == EOF)
11487f
-			return (EOF);
11487f
-	}
11487f
-	else {
11487f
-		ch = get_number(&num1, low, names, ch, file, ",- \t\n");
11487f
-		if (ch == EOF)
11487f
-			return (EOF);
11487f
+			case R_STEP:
11487f
+				if (get_number(&num3, 0, PPC_NULL, file) != EOF) {
11487f
+					state = R_TERMS;
11487f
+					break;
11487f
+				}
11487f
+				return (EOF);
11487f
 
11487f
-		if (ch != '-') {
11487f
-			/* not a range, it's a single number.
11487f
-			 */
11487f
-			if (EOF == set_element(bits, low, high, num1)) {
11487f
-				unget_char(ch, file);
11487f
+			case R_TERMS:
11487f
+				if (is_separator(ch)) {
11487f
+					state = R_FINISH;
11487f
+					break;
11487f
+				}
11487f
 				return (EOF);
11487f
-			}
11487f
-			return (ch);
11487f
-		}
11487f
-		else {
11487f
-			/* eat the dash
11487f
-			 */
11487f
-			ch = get_char(file);
11487f
-			if (ch == EOF)
11487f
+
11487f
+			case R_NUM1:
11487f
+				if (ch == '-') {
11487f
+					state = R_RANGE;
11487f
+					break;
11487f
+				}
11487f
+				if (ch == '~') {
11487f
+					state = R_RANDOM;
11487f
+					break;
11487f
+				}
11487f
+				if (is_separator(ch)) {
11487f
+					num2 = num1;
11487f
+					state = R_FINISH;
11487f
+					break;
11487f
+				}
11487f
 				return (EOF);
11487f
 
11487f
-			/* get the number following the dash
11487f
-			 */
11487f
-			ch = get_number(&num2, low, names, ch, file, "/, \t\n");
11487f
-			if (ch == EOF || num1 > num2)
11487f
+			case R_RANGE:
11487f
+				if (get_number(&num2, low, names, file) != EOF) {
11487f
+					state = R_RANGE_NUM2;
11487f
+					break;
11487f
+				}
11487f
 				return (EOF);
11487f
-		}
11487f
-	}
11487f
 
11487f
-	/* check for step size
11487f
-	 */
11487f
-	if (ch == '/') {
11487f
-		/* eat the slash
11487f
-		 */
11487f
-		ch = get_char(file);
11487f
-		if (ch == EOF)
11487f
-			return (EOF);
11487f
+			case R_RANGE_NUM2:
11487f
+				if (ch == '/') {
11487f
+					state = R_STEP;
11487f
+					break;
11487f
+				}
11487f
+				if (is_separator(ch)) {
11487f
+					state = R_FINISH;
11487f
+					break;
11487f
+				}
11487f
+				return (EOF);
11487f
 
11487f
-		/* get the step size -- note: we don't pass the
11487f
-		 * names here, because the number is not an
11487f
-		 * element id, it's a step size.  'low' is
11487f
-		 * sent as a 0 since there is no offset either.
11487f
-		 */
11487f
-		ch = get_number(&num3, 0, PPC_NULL, ch, file, ", \t\n");
11487f
-		if (ch == EOF || num3 == 0)
11487f
-			return (EOF);
11487f
-	}
11487f
-	else {
11487f
-		/* no step.  default==1.
11487f
-		 */
11487f
-		num3 = 1;
11487f
-	}
11487f
+			case R_RANDOM:
11487f
+				if (is_separator(ch)) {
11487f
+					num2 = high;
11487f
+					state = R_FINISH;
11487f
+				}
11487f
+				else if (unget_char(ch, file),
11487f
+						get_number(&num2, low, names, file) != EOF) {
11487f
+					state = R_TERMS;
11487f
+				}
11487f
+				/* fail if couldn't find match on previous term
11487f
+				 */
11487f
+				else
11487f
+					return (EOF);
11487f
 
11487f
-	/* num1 (through i) will be validated by set_element() below, but num2
11487f
-	 * and num3 are merely used as loop condition and increment, and must
11487f
-	 * be validated separately.
11487f
-	 */
11487f
-	if (num2 < low || num2 > high || num3 > high)
11487f
+				/* if invalid random range was selected */
11487f
+				if (num1 > num2)
11487f
+					return (EOF);
11487f
+
11487f
+				/* select random number in range <num1, num2>
11487f
+				 */
11487f
+				num1 = num2 = random() % (num2 - num1 + 1) + num1;
11487f
+				break;
11487f
+
11487f
+
11487f
+			default:
11487f
+				/* We should never get here
11487f
+				 */
11487f
+				return (EOF);
11487f
+		}
11487f
+	}
11487f
+	if (state != R_FINISH || ch == EOF)
11487f
 		return (EOF);
11487f
 
11487f
-	/* range. set all elements from num1 to num2, stepping
11487f
-	 * by num3.  (the step is a downward-compatible extension
11487f
-	 * proposed conceptually by bob@acornrc, syntactically
11487f
-	 * designed then implemented by paul vixie).
11487f
-	 */
11487f
 	for (i = num1; i <= num2; i += num3)
11487f
 		if (EOF == set_element(bits, low, high, i)) {
11487f
 			unget_char(ch, file);
11487f
 			return (EOF);
11487f
 		}
11487f
-
11487f
-	return (ch);
11487f
+	return ch;
11487f
 }
11487f
 
11487f
 static int
11487f
-get_number(int *numptr, int low, const char *names[], int ch, FILE * file,
11487f
-	const char *terms) {
11487f
+get_number(int *numptr, int low, const char *names[], FILE * file) {
11487f
 	char temp[MAX_TEMPSTR], *pc;
11487f
-	int len, i;
11487f
+	int len, i, ch;
11487f
+	char *endptr;
11487f
 
11487f
 	pc = temp;
11487f
 	len = 0;
11487f
 
11487f
-	/* first look for a number */
11487f
-	while (isdigit((unsigned char) ch)) {
11487f
+	/* get all alnum characters available */
11487f
+	while (isalnum((ch = get_char(file)))) {
11487f
 		if (++len >= MAX_TEMPSTR)
11487f
 			goto bad;
11487f
 		*pc++ = (char)ch;
11487f
-		ch = get_char(file);
11487f
 	}
11487f
-	*pc = '\0';
11487f
-	if (len != 0) {
11487f
-		/* got a number, check for valid terminator */
11487f
-		if (!strchr(terms, ch))
11487f
-			goto bad;
11487f
-		*numptr = atoi(temp);
11487f
-		return (ch);
11487f
+	if (len == 0)
11487f
+		goto bad;
11487f
+
11487f
+	unget_char(ch, file);
11487f
+
11487f
+	/* try to get number */
11487f
+	*numptr = (int) strtol(temp, &endptr, 10);
11487f
+	if (*endptr == '\0' && temp != endptr) {
11487f
+		/* We have a number */
11487f
+		return 0;
11487f
 	}
11487f
 
11487f
 	/* no numbers, look for a string if we have any */
11487f
 	if (names) {
11487f
-		while (isalpha((unsigned char) ch)) {
11487f
-			if (++len >= MAX_TEMPSTR)
11487f
-				goto bad;
11487f
-			*pc++ = (char)ch;
11487f
-			ch = get_char(file);
11487f
-		}
11487f
-		*pc = '\0';
11487f
-		if (len != 0 && strchr(terms, ch)) {
11487f
-			for (i = 0; names[i] != NULL; i++) {
11487f
-				Debug(DPARS | DEXT,
11487f
-					("get_num, compare(%s,%s)\n", names[i], temp));
11487f
-				if (!strcasecmp(names[i], temp)) {
11487f
-					*numptr = i + low;
11487f
-					return (ch);
11487f
-				}
11487f
+		for (i = 0; names[i] != NULL; i++) {
11487f
+			Debug(DPARS | DEXT, ("get_num, compare(%s,%s)\n", names[i], temp));
11487f
+			if (strcasecmp(names[i], temp) == 0) {
11487f
+				*numptr = i + low;
11487f
+				return 0;
11487f
 			}
11487f
 		}
11487f
+	} else {
11487f
+		goto bad;
11487f
 	}
11487f
 
11487f
   bad:
11487f
-- 
11487f
2.36.1
11487f