Add support for "~" ("random within range")

Resolves: rhbz#2090691
This commit is contained in:
Ondřej Pohořelský 2022-05-27 15:06:56 +02:00
parent 7421ea58d6
commit 11516b05d1
5 changed files with 462 additions and 1 deletions

View File

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

View File

@ -0,0 +1,25 @@
From 0589b06aa369efd3cd5dfc0bba9a868f48a14506 Mon Sep 17 00:00:00 2001
From: Tomas Mraz <tmraz@fedoraproject.org>
Date: Wed, 5 Jan 2022 19:17:18 +0100
Subject: [PATCH 3/5] get_number: Add missing NUL termination for the scanned
string
---
src/entry.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/entry.c b/src/entry.c
index f2bb717..15ce9b5 100644
--- a/src/entry.c
+++ b/src/entry.c
@@ -666,6 +666,7 @@ get_number(int *numptr, int low, const char *names[], FILE * file) {
goto bad;
*pc++ = (char)ch;
}
+ *pc = '\0';
if (len == 0)
goto bad;
--
2.36.1

View File

@ -0,0 +1,28 @@
From 991a5f2a44c68f576b6c6da3a7ac8fbc8f97a3b0 Mon Sep 17 00:00:00 2001
From: Tomas Mraz <tmraz@fedoraproject.org>
Date: Tue, 22 Mar 2022 14:35:48 +0100
Subject: [PATCH 4/5] Fix regression in handling */x crontab entries
Fixes #102
---
src/entry.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/entry.c b/src/entry.c
index 15ce9b5..e9e258b 100644
--- a/src/entry.c
+++ b/src/entry.c
@@ -563,7 +563,9 @@ get_range(bitstr_t * bits, int low, int high, const char *names[],
return (EOF);
case R_STEP:
- if (get_number(&num3, 0, PPC_NULL, file) != EOF) {
+ unget_char(ch, file);
+ if (get_number(&num3, 0, PPC_NULL, file) != EOF
+ && num3 != 0) {
state = R_TERMS;
break;
}
--
2.36.1

View File

@ -0,0 +1,24 @@
From d1a4e2b1a091df104881a6dcd0e41d805c86cb1a Mon Sep 17 00:00:00 2001
From: w30023233 <wangyuhang27@huawei.com>
Date: Wed, 23 Mar 2022 15:40:01 +0800
Subject: [PATCH 5/5] Fix regression in handling 1-5 crontab entries
---
src/entry.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/entry.c b/src/entry.c
index e9e258b..bb7cb62 100644
--- a/src/entry.c
+++ b/src/entry.c
@@ -595,6 +595,7 @@ get_range(bitstr_t * bits, int low, int high, const char *names[],
return (EOF);
case R_RANGE:
+ unget_char(ch, file);
if (get_number(&num2, low, names, file) != EOF) {
state = R_RANGE_NUM2;
break;
--
2.36.1

View File

@ -6,12 +6,17 @@
Summary: Cron daemon for executing programs at set times
Name: cronie
Version: 1.5.7
Release: 5%{?dist}
Release: 6%{?dist}
License: MIT and BSD and ISC and GPLv2+
URL: https://github.com/cronie-crond/cronie
Source0: https://github.com/cronie-crond/cronie/releases/download/cronie-%{version}/cronie-%{version}.tar.gz
Patch: 0001-Address-issues-found-by-coverity-scan.patch
# Add support for "~" ("random within range") + regression fixing patches (rhbz#2090691)
Patch: 0002-Add-random-within-range-operator.patch
Patch: 0003-get_number-Add-missing-NUL-termination-for-the-scann.patch
Patch: 0004-Fix-regression-in-handling-x-crontab-entries.patch
Patch: 0005-Fix-regression-in-handling-1-5-crontab-entries.patch
Requires: dailyjobs
@ -206,6 +211,10 @@ exit 0
%attr(0644,root,root) %config(noreplace) %{_sysconfdir}/cron.d/dailyjobs
%changelog
* Fri May 27 2022 Ondřej Pohořelský <opohorel@redhat.com> - 1.5.7-6
- Add support for "~" ("random within range")
Resolves: rhbz#2090691
* Mon Aug 09 2021 Mohan Boddu <mboddu@redhat.com> - 1.5.7-5
- Rebuilt for IMA sigs, glibc 2.34, aarch64 flags
Related: rhbz#1991688