Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement support for multiple 'H' ranges in one field #3

Merged
merged 14 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.28)
project(ccronexpr C)

set(CMAKE_C_STANDARD 11)
add_compile_definitions(CRON_TEST_MALLOC=1)

include_directories(.)

Expand Down
205 changes: 143 additions & 62 deletions ccronexpr.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <errno.h>
#include <limits.h>
#include <string.h>
#include <stdarg.h>

#include "ccronexpr.h"

Expand Down Expand Up @@ -883,7 +884,7 @@ static char* str_replace(char *orig, const char *rep, const char *with) {
static unsigned int parse_uint(const char* str, int* errcode) {
char* endptr;
errno = 0;
long int l = strtol(str, &endptr, 0);
long int l = strtol(str, &endptr, 10);
if (errno == ERANGE || *endptr != '\0' || l < 0 || l > INT_MAX) {
*errcode = 1;
return 0;
Expand Down Expand Up @@ -1017,7 +1018,9 @@ void cron_init_custom_hash_fn(cron_custom_hash_fn func)
}

/**
* Replace H parameter with integer in proper range. If using an iterator fielo, min/max have to be set to proper values before!
* Replace H parameter with integer in proper range. If using an iterator field, min/max have to be set to proper values before!
* The input field will always be freed, the returned char* should be used instead.
*
* @param field CRON field which needs a value for its 'H' (in string form)
* @param n Position of the field in the CRON string, from 0 - 5
* @param min Minimum value allowed in field/for replacement
Expand Down Expand Up @@ -1254,77 +1257,147 @@ void set_number_hits(const char* value, uint8_t* target, unsigned int min, unsig

}

static char* check_and_replace_h(char* field, unsigned int pos, unsigned int min, const char** error)
{
static char *replace_h_entry(char *field, unsigned int pos, unsigned int min, const char **error) {
char* has_h = strchr(field, 'H');
if (has_h == NULL) {
return field;
}

unsigned int fieldMax = 0, customMax = 0;
// minBuf is 0xFF to see if it has been altered/read successfully, since 0 is a valid value for it
unsigned int minBuf = 0xFF, maxBuf = 0;
char* has_h = strchr(field, 'H');
if (has_h) {
if ( *(has_h+1) == '/') { /* H before an iterator */
sscanf(has_h, "H/%2u", &customMax); // get value of iterator, so it will be used as maximum instead of standard maximum for field
if (!customMax) { /* iterator might have been specified as an ordinal instead... */
*error = "Hashed: Iterator error";
return field;
}
}
if ( (has_h != field) && (*(has_h-1) == '/') ) { /* H not allowed as iterator */
*error = "Hashed: 'H' not allowed as iterator";
return field;
}
if ( *(has_h+1) =='-' || \
( has_h != field && *(has_h-1) == '-') ) { // 'H' not starting field, so may be the end of a range
*error = "'H' is not allowed for use in ranges";

if(*(has_h + 1) == '/') { /* H before an iterator */
sscanf(has_h, "H/%2u", &customMax); // get value of iterator, so it will be used as maximum instead of standard maximum for field
if (!customMax) { /* iterator might have been specified as an ordinal instead... */
*error = "Hashed: Iterator error";
return field;
}
// Test if custom Range is specified
if ( *(has_h+1) == '(' ) {
sscanf(has_h, "H(%2u-%2u)", &minBuf, &maxBuf);
if ( !maxBuf || \
}
if ((has_h != field) && (*(has_h - 1) == '/') ) { /* H not allowed as iterator */
*error = "Hashed: 'H' not allowed as iterator";
return field;
}
if (*(has_h + 1) == '-' || \
(has_h != field && *(has_h - 1) == '-') ) { // 'H' not starting field, so may be the end of a range
*error = "'H' is not allowed for use in ranges";
return field;
}
// Test if custom Range is specified
if (*(has_h + 1) == '(' ) {
sscanf(has_h, "H(%2u-%2u)", &minBuf, &maxBuf);
if ( !maxBuf || \
(minBuf == 0xFF) || \
(minBuf > maxBuf) || \
(minBuf < min) || \
// if a customMax is present: Is read maximum bigger than it? (which it shouldn't be)
(customMax ? maxBuf > customMax : 0)
) {
*error = "'H' custom range error";
return field;
}
min = minBuf;
// maxBuf needs to be incremented by 1 to include it
customMax = maxBuf + 1;
}
switch (pos) {
case CRON_FIELD_SECOND:
fieldMax = CRON_MAX_SECONDS;
break;
case CRON_FIELD_MINUTE:
fieldMax = CRON_MAX_MINUTES;
break;
case CRON_FIELD_HOUR:
fieldMax = CRON_MAX_HOURS;
break;
case CRON_FIELD_DAY_OF_MONTH:
// limited to 28th so the hashed cron will be executed every month
fieldMax = 28;
break;
case CRON_FIELD_MONTH:
fieldMax = CRON_MAX_MONTHS;
break;
case CRON_FIELD_DAY_OF_WEEK:
fieldMax = CRON_MAX_DAYS_OF_WEEK;
break;
default:
*error = "Unknown field!";
return field;
(customMax ? maxBuf > customMax : 0)
) {
*error = "'H' custom range error";
return field;
}
if (!customMax) {
customMax = fieldMax;
} else if (customMax > fieldMax) {
*error = "'H' range maximum error";
min = minBuf;
// maxBuf needs to be incremented by 1 to include it
customMax = maxBuf + 1;
}
switch (pos) {
case CRON_FIELD_SECOND:
fieldMax = CRON_MAX_SECONDS;
break;
case CRON_FIELD_MINUTE:
fieldMax = CRON_MAX_MINUTES;
break;
case CRON_FIELD_HOUR:
fieldMax = CRON_MAX_HOURS;
break;
case CRON_FIELD_DAY_OF_MONTH:
// limited to 28th so the hashed cron will be executed every month
fieldMax = 28;
break;
case CRON_FIELD_MONTH:
fieldMax = CRON_MAX_MONTHS;
break;
case CRON_FIELD_DAY_OF_WEEK:
fieldMax = CRON_MAX_DAYS_OF_WEEK;
break;
default:
*error = "Unknown field!";
return field;
}
if (!customMax) {
customMax = fieldMax;
} else if (customMax > fieldMax) {
*error = "'H' range maximum error";
return field;
}
field = replace_hashed(field, pos, min, customMax, fn, error);

return field;
}

static char* check_and_replace_h(char* field, unsigned int pos, unsigned int min, const char** error)
{
char* has_h = strchr(field, 'H');
if (has_h) {
char *accum_field = NULL;
char **subfields = NULL;
size_t subfields_len = 0;
// Check if Field contains ',', if so, split into multiple subfields, and replace in each (with same position no)
char *has_comma = strchr(field, ',');
if (has_comma) {
// Iterate over split sub-fields, check for 'H' and replace if present
subfields = split_str(field, ',', &subfields_len);
if (subfields == NULL) {
*error = "Failed to split 'H' string in list";
goto return_error;
}
size_t res_len = 0;
size_t res_lens[subfields_len];
for (size_t i = 0; i < subfields_len; i++) {
has_h = strchr(subfields[i], 'H');
if (has_h) {
subfields[i] = replace_h_entry(subfields[i], pos, min, error);
}
if (*error != NULL) {
goto return_error;
}
res_lens[i] = strnlen(subfields[i], CRON_MAX_STR_LEN_TO_SPLIT);
res_len += res_lens[i];
}
// Allocate space for the full string: Result lengths + (result count - 1) for the commas + 1 for '\0'
accum_field = (char *) cronMalloc(res_len + subfields_len );
if (accum_field == NULL) {
*error = "Failed to merge 'H' in list";
goto return_error;
}
memset(accum_field, 0, res_len + subfields_len);
char *tracking = accum_field;
for (size_t i = 0; i < subfields_len; i++) {
// Sanity check: Is "tracking" still in the allocated memory boundaries?
if ((tracking - accum_field) > (res_len + subfields_len)) {
*error = "Failed to insert subfields to merged fields: String went oob";
goto return_error;
}
strncpy(tracking, subfields[i], res_lens[i]);
tracking += res_lens[i];
// Don't append comma to last list entry
if (i < subfields_len-1) {
strncpy(tracking, ",", 2); // using 2 to ensure the string ends in '\0', tracking will be set to that char
tracking += 1;
}
}
free_splitted(subfields, subfields_len);
cronFree(field);
return accum_field;
}
field = replace_hashed(field, pos, min, customMax, fn, error);
// only one H to find and replace, then return
field = replace_h_entry(field, pos, min, error);
return field;

return_error:
if (subfields) free_splitted(subfields, subfields_len);
if (accum_field) cronFree(accum_field);
return field;
}
return field;
}
Expand Down Expand Up @@ -1468,6 +1541,7 @@ static char* w_check(char* field, cron_expr* target, const char** error)
goto return_error;
}
memset(newField, 0, sizeof(char) * strlen(field));
char *tracking = newField;
// Ensure only 1 day is specified, and W day is not the last in a range or list or iterator of days
if ( has_char(field, '/') || has_char(field, '-')) {
*error = "W not allowed in iterators or ranges in 'day of month' field";
Expand Down Expand Up @@ -1502,7 +1576,14 @@ static char* w_check(char* field, cron_expr* target, const char** error)
cron_setBit(target->w_flags, w_day);
}
} else {
strcat(newField, splitField[i]);
if (tracking != newField) {
// A field was already added. Add a comma first
strncpy(tracking, ",", 2); // ensure string ends in '\0', tracking will be set to it
tracking += 1;
}
size_t field_len = strnlen(splitField[i], CRON_MAX_STR_LEN_TO_SPLIT);
strncpy(tracking, splitField[i], field_len);
tracking += field_len;
}
}
free_splitted(splitField, len_out);
Expand Down
Loading