From 11516b05d105c3a69f362fded4803b18100f049b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Poho=C5=99elsk=C3=BD?= Date: Fri, 27 May 2022 15:06:56 +0200 Subject: [PATCH] Add support for "~" ("random within range") Resolves: rhbz#2090691 --- 0002-Add-random-within-range-operator.patch | 375 ++++++++++++++++++ ...issing-NUL-termination-for-the-scann.patch | 25 ++ ...ession-in-handling-x-crontab-entries.patch | 28 ++ ...sion-in-handling-1-5-crontab-entries.patch | 24 ++ cronie.spec | 11 +- 5 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 0002-Add-random-within-range-operator.patch create mode 100644 0003-get_number-Add-missing-NUL-termination-for-the-scann.patch create mode 100644 0004-Fix-regression-in-handling-x-crontab-entries.patch create mode 100644 0005-Fix-regression-in-handling-1-5-crontab-entries.patch diff --git a/0002-Add-random-within-range-operator.patch b/0002-Add-random-within-range-operator.patch new file mode 100644 index 0000000..45dbf9f --- /dev/null +++ b/0002-Add-random-within-range-operator.patch @@ -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 = 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 + diff --git a/0003-get_number-Add-missing-NUL-termination-for-the-scann.patch b/0003-get_number-Add-missing-NUL-termination-for-the-scann.patch new file mode 100644 index 0000000..9faa394 --- /dev/null +++ b/0003-get_number-Add-missing-NUL-termination-for-the-scann.patch @@ -0,0 +1,25 @@ +From 0589b06aa369efd3cd5dfc0bba9a868f48a14506 Mon Sep 17 00:00:00 2001 +From: Tomas Mraz +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 + diff --git a/0004-Fix-regression-in-handling-x-crontab-entries.patch b/0004-Fix-regression-in-handling-x-crontab-entries.patch new file mode 100644 index 0000000..3ad494f --- /dev/null +++ b/0004-Fix-regression-in-handling-x-crontab-entries.patch @@ -0,0 +1,28 @@ +From 991a5f2a44c68f576b6c6da3a7ac8fbc8f97a3b0 Mon Sep 17 00:00:00 2001 +From: Tomas Mraz +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 + diff --git a/0005-Fix-regression-in-handling-1-5-crontab-entries.patch b/0005-Fix-regression-in-handling-1-5-crontab-entries.patch new file mode 100644 index 0000000..6152ef6 --- /dev/null +++ b/0005-Fix-regression-in-handling-1-5-crontab-entries.patch @@ -0,0 +1,24 @@ +From d1a4e2b1a091df104881a6dcd0e41d805c86cb1a Mon Sep 17 00:00:00 2001 +From: w30023233 +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 + diff --git a/cronie.spec b/cronie.spec index 81cc235..1abd205 100644 --- a/cronie.spec +++ b/cronie.spec @@ -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ý - 1.5.7-6 +- Add support for "~" ("random within range") + Resolves: rhbz#2090691 + * Mon Aug 09 2021 Mohan Boddu - 1.5.7-5 - Rebuilt for IMA sigs, glibc 2.34, aarch64 flags Related: rhbz#1991688