From f50bc4a3c48481f088cb215def0b64b83dc5bd82 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Tue, 17 Jan 2023 23:09:06 +0100 Subject: [PATCH] chore: added cron code --- src/cron/expression.c | 118 +++++++++++++++ src/cron/expression.h | 50 +++++++ src/cron/parse.c | 335 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 503 insertions(+) create mode 100644 src/cron/expression.c create mode 100644 src/cron/expression.h create mode 100644 src/cron/parse.c diff --git a/src/cron/expression.c b/src/cron/expression.c new file mode 100644 index 0000000..7d27be6 --- /dev/null +++ b/src/cron/expression.c @@ -0,0 +1,118 @@ +#include "expression.h" +#include + +const uint8_t month_days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +cron_expression *ce_init() { return malloc(sizeof(cron_expression)); } + +void ce_free(cron_expression *ce) { + free(ce->months); + free(ce->days); + free(ce->hours); + free(ce->minutes); + free(ce); +} + +void ce_next(cron_simple_time *out, cron_expression *ce, + cron_simple_time *ref) { + // For all of these values, the rule is the following: if their value is + // the length of their respective array in the CronExpression object, that + // means we've looped back around. This means that the "bigger" value has + // to be incremented by one. For example, if the minutes have looped + // around, that means that the hour has to be incremented as well. + uint8_t month_index = 0; + uint8_t day_index = 0; + uint8_t hour_index = 0; + uint8_t minute_index = 0; + + // This chain is the same logic multiple times, namely that if a "bigger" + // value loops around, then the smaller value will always reset as well. + // For example, if we're going to a new day, the hour & minute will always + // be their smallest value again. + while (month_index < ce->month_count && + ref->month > ce->months[month_index]) { + month_index++; + } + + if (month_index < ce->month_count && + ref->month == ce->months[month_index]) { + while (day_index < ce->day_count && ref->day > ce->days[day_index]) { + day_index++; + } + + if (day_index < ce->day_count && ref->day == ce->days[day_index]) { + while (hour_index < ce->hour_count && + ref->hour > ce->hours[hour_index]) { + hour_index++; + } + + if (hour_index < ce->hour_count && + ref->hour == ce->hours[hour_index]) { + // Minute is the only value where we explicitely make sure we + // can't match sref's value exactly. This is to ensure we only + // return values in the future. + while (minute_index < ce->minute_count && + ref->minute >= ce->minutes[minute_index]) { + minute_index++; + } + } + } + } + + // Here, we increment the "bigger" values by one if the smaller ones loop + // around. The order is important, as it allows a sort-of waterfall effect + // to occur which updates all values if required. + if (minute_index == ce->minute_count && hour_index < ce->hour_count) { + hour_index++; + } + + if (hour_index == ce->hour_count && day_index < ce->day_count) { + day_index++; + } + + if (day_index == ce->day_count && month_index < ce->month_count) { + month_index++; + } + + out->minute = ce->minutes[minute_index % ce->minute_count]; + out->hour = ce->hours[hour_index % ce->hour_count]; + out->day = ce->days[day_index % ce->day_count]; + + // Sometimes, we end up with a day that does not exist within the selected + // month, e.g. day 30 in February. When this occurs, we reset day back to + // the smallest value & loop over to the next month that does have this + // day. + if (out->day > month_days[ce->months[month_index % ce->month_count] - 1]) { + out->day = ce->days[0]; + month_index++; + + while (out->day > + month_days[ce->months[month_index % ce->month_count] - 1]) { + month_index++; + } + } + + out->month = ce->months[month_index % ce->month_count]; + + if (month_index >= ce->month_count) { + out->year = ref->year + 1; + } else { + out->year = ref->year; + } +} + +void ce_next_from_now(cron_simple_time *out, cron_expression *ce) { + time_t t = time(NULL); + struct tm gm; + gmtime_r(&t, &gm); + + cron_simple_time ref = {// tm_year contains years since 1900 + .year = 1900 + gm.tm_year, + // tm_mon goes from 0 to 11 + .month = gm.tm_mon + 1, + .day = gm.tm_mday, + .hour = gm.tm_hour, + .minute = gm.tm_min}; + + ce_next(out, ce, &ref); +} diff --git a/src/cron/expression.h b/src/cron/expression.h new file mode 100644 index 0000000..9d378bd --- /dev/null +++ b/src/cron/expression.h @@ -0,0 +1,50 @@ +#ifndef VIETER_CRON +#define VIETER_CRON + +#include +#include +#include +#include +#include + +typedef enum cron_parse_error { + cron_parse_ok = 0, + cron_parse_invalid_expression = 1, + cron_parse_invalid_number = 2, + cron_parse_out_of_range = 3, + cron_parse_too_many_parts = 4, + cron_parse_not_enough_parts = 5 +} cron_parse_error; + +typedef struct cron_expression { + uint8_t *minutes; + uint8_t *hours; + uint8_t *days; + uint8_t *months; + uint8_t minute_count; + uint8_t hour_count; + uint8_t day_count; + uint8_t month_count; +} cron_expression; + +typedef struct cron_simple_time { + int year; + int month; + int day; + int hour; + int minute; +} cron_simple_time; + +cron_expression *ce_init(); + +void cron_ce_free(cron_expression *ce); + +void cron_ce_next(cron_simple_time *out, cron_expression *ce, + cron_simple_time *ref); + +void cron_ce_next_from_now(cron_simple_time *out, cron_expression *ce); + +enum cron_parse_error cron_ce_parse_expression(cron_expression *out, + const char *expression); + +#endif diff --git a/src/cron/parse.c b/src/cron/parse.c new file mode 100644 index 0000000..6a8bdc1 --- /dev/null +++ b/src/cron/parse.c @@ -0,0 +1,335 @@ +#include "expression.h" + +// This prefix is needed to properly compile +const uint8_t parse_month_days[] = {31, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31}; + +// Allowed value ranges for the minute, hour, day and month field +const uint8_t min[4] = {0, 0, 1, 1}; +const uint8_t max[4] = {59, 23, 31, 12}; + +const uint8_t min_parts = 2; +const uint8_t max_parts = 4; + +// Convert a string into a uint8_t value by parsing it using atoi and checking +// whether it's contained within the given range +#define SAFE_ATOI(v, s, min, max) \ + int _##v = atoi(s); \ + if ((_##v) == 0 && strcmp((s), "0") != 0) { \ + return cron_parse_invalid_number; \ + } \ + if (((_##v) < (min)) || ((_##v) > (max))) { \ + return cron_parse_out_of_range; \ + } \ + v = (uint8_t)(_##v); + +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) + +/** + * Given a range expression, produce a bit field defining what numbers in the + * min-max range the expression represents. Bit 0 (starting from the + * right) corresponds to min, the bit max - min to max. All trailing bits + * after this should be ignored. The given bitfield is modified in-place, so + * multiple calls of this function can be performed on the same value to create + * the effect of ORing their values. + * + * A range expression has one of the following forms: + * + * - * + * - a + * - a-b + * - a/c + * - a-b/c + */ +cron_parse_error ce_parse_range(uint64_t *out, char *s, uint8_t min, + uint8_t max) { + size_t slash_index = 0, dash_index = 0; + size_t s_index = 0; + char cur_char; + bool is_valid_character; + + while ((cur_char = s[s_index]) != '\0') { + is_valid_character = cur_char == '/' || cur_char == '-' || + cur_char == '*' || + (cur_char >= '0' && cur_char <= '9'); + + if (!is_valid_character) { + return cron_parse_invalid_expression; + } + + if (cur_char == '/') { + if (s_index == 0 || slash_index != 0) { + return cron_parse_invalid_expression; + } + + slash_index = s_index; + + s[s_index] = '\0'; + } else if (cur_char == '-') { + // At most one dash is allowed, and it must be before the slash + if (s_index == 0 || dash_index != 0 || slash_index != 0) { + return cron_parse_invalid_expression; + } + + dash_index = s_index; + + s[s_index] = '\0'; + } + + s_index++; + } + + uint8_t start; + uint8_t end = max; + uint8_t interval = 0; + + if (s[0] == '*') { + if (s[1] != '\0' || dash_index != 0) { + return cron_parse_invalid_expression; + } + + start = min; + interval = 1; + } else { + SAFE_ATOI(start, s, min, max); + + if (dash_index > 0) { + SAFE_ATOI(end, &s[dash_index + 1], min, max); + interval = 1; + } + } + + if (slash_index > 0) { + SAFE_ATOI(interval, &s[slash_index + 1], 1, max - min); + } + + if (interval == 0) { + *out |= ((uint64_t)1) << (start - min); + } else { + while (start <= end) { + *out |= ((uint64_t)1) << (start - min); + start += interval; + } + } + + return cron_parse_ok; +} + +/* + * Given an expression part, produce a bitfield defining what numbers in the + * min-max range the part represents. A part consists of one or more range + * expressions, separated by commas. + */ +cron_parse_error ce_parse_part(uint64_t *out, char *s, uint8_t min, + uint8_t max) { + *out = 0; + + char *next; + cron_parse_error res; + + while ((next = strchr(s, ',')) != NULL) { + next[0] = '\0'; + + res = ce_parse_range(out, s, min, max); + + if (res != cron_parse_ok) { + return res; + } + + s = next + 1; + } + + // Make sure to parse the final range as well + return ce_parse_range(out, s, min, max); +} + +/* + * Return how many bits are set in the bitfield, better known as popcount. I + * added my own implementation (taken from my algorithms course) as I don't want + * to be dependent on GCC-specific extensions. + */ +uint8_t uint64_t_popcount(uint64_t n) { + uint8_t set_bits = 0; + + while (n != 0) { + // This sets the least significant bit to zero (very cool) + n &= n - 1; + + set_bits++; + } + + return set_bits; +} + +/* + * Convert a bitfield into an array containing the numbers in the min-max range + * it represents. + */ +uint8_t bf_to_nums(uint8_t **out, uint64_t bf, uint8_t min, uint8_t max) { + // Each bit field only has `max - min + 1` meaningful bits. All other bits + // should be ignored, and can be any value. By shifting the bit field back + // and forth, we set these excessive bits to zero, ensuring popcount returns + // the correct value. + uint8_t excess_bits = 64 - (max - min + 1); + bf = (bf << excess_bits) >> excess_bits; + + uint8_t size = uint64_t_popcount(bf); + uint8_t *buf = malloc(size * sizeof(uint8_t)); + + uint8_t bit_index = 0, buf_index = 0; + + while (buf_index < size && bit_index <= max - min) { + if (((uint64_t)1 << bit_index) & bf) { + // Resize buffer if needed + buf[buf_index] = min + bit_index; + buf_index++; + } + + bit_index++; + } + + *out = buf; + + return size; +} + +/* + * Parse a cron expression string into a cron_expression struct. + */ +cron_parse_error ce_parse_expression(cron_expression *out, + const char *expression) { + // The parsing functions modify the input string in-place + char *s = strdup(expression); + char *orig_s = s; + + cron_parse_error res = cron_parse_ok; + + // First we divide the input string into its parts, divided by spaces. + // Each part is delimited by a NULL byte. + uint8_t part_count = 0; + char *parts[max_parts]; + char *next_space; + + // Skip leading spaces + size_t offset = 0; + + while (s[offset] == ' ') { + offset++; + } + + s += offset; + + while (part_count < max_parts && ((next_space = strchr(s, ' ')) != NULL)) { + next_space[0] = '\0'; + + parts[part_count] = s; + part_count++; + + // Skip multiple spaces + offset = 1; + while (next_space[offset] == ' ') { + offset++; + } + s = next_space + offset; + } + + // Each iteration of the loop skips all trailing spaces. This means that, if + // s[0] isn't '\0', there's still another part before the end of the string. + if (s[0] != '\0') { + if (part_count == max_parts) { + res = cron_parse_too_many_parts; + goto end; + } + + parts[part_count] = s; + part_count++; + } + + if (part_count < min_parts) { + res = cron_parse_not_enough_parts; + goto end; + } + + // We now parse the parts in reverse. This is because the month part + // determines the maximum value of the day part. + + uint64_t bit_field = 0; + + // Months + if (part_count >= 4) { + res = ce_parse_part(&bit_field, parts[3], min[3], max[3]); + + if (res != cron_parse_ok) { + goto end; + } + + out->month_count = bf_to_nums(&out->months, bit_field, min[3], max[3]); + } + // If months aren't provided, they're replaced with a * + else { + out->month_count = bf_to_nums(&out->months, ~0, min[3], max[3]); + } + + // Determine what the largest allowed day value is, given the months + uint8_t max_day_value = 0; + + for (uint8_t i = 0; i < out->month_count; i++) { + max_day_value = + MAX(max_day_value, parse_month_days[out->months[i] - 1]); + } + + // Days + if (part_count >= 3) { + bit_field = 0; + + res = ce_parse_part(&bit_field, parts[2], min[2], max_day_value); + + if (res != cron_parse_ok) { + free(out->months); + + goto end; + } + + out->day_count = + bf_to_nums(&out->days, bit_field, min[2], max_day_value); + } + // If days aren't provided, they're replaced with a * + else { + out->day_count = bf_to_nums(&out->days, ~0, min[2], max_day_value); + } + + // Hours + bit_field = 0; + + res = ce_parse_part(&bit_field, parts[1], min[1], max[1]); + + if (res != cron_parse_ok) { + free(out->months); + free(out->days); + + goto end; + } + + out->hour_count = bf_to_nums(&out->hours, bit_field, min[1], max[1]); + + // Minutes + bit_field = 0; + + res = ce_parse_part(&bit_field, parts[0], min[0], max[0]); + + if (res != cron_parse_ok) { + free(out->months); + free(out->days); + free(out->hours); + + goto end; + } + + out->minute_count = bf_to_nums(&out->minutes, bit_field, min[0], max[0]); + +end: + // s is cloned + free(orig_s); + + return res; +}