From d0b5314619736c093b95d5a131463e21d1ac4603 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Thu, 12 Jan 2023 12:26:12 +0100 Subject: [PATCH] feat(cron): mostly written C expression parser --- .gitignore | 2 +- src/cron/expression/c/expression.h | 22 ++++ src/cron/expression/c/parse.c | 179 +++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 src/cron/expression/c/expression.h create mode 100644 src/cron/expression/c/parse.c diff --git a/.gitignore b/.gitignore index aaec9ef..daeb3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -*.c +vieter.c /data/ # Build artifacts diff --git a/src/cron/expression/c/expression.h b/src/cron/expression/c/expression.h new file mode 100644 index 0000000..5b8e4f9 --- /dev/null +++ b/src/cron/expression/c/expression.h @@ -0,0 +1,22 @@ +#include +#include +#include +#include + +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; +} CronExpression; + +/** + * Given a + */ +int ce_next(struct tm *out, struct tm *ref); + +int ce_parse(CronExpression *out, char *s); diff --git a/src/cron/expression/c/parse.c b/src/cron/expression/c/parse.c new file mode 100644 index 0000000..17d416f --- /dev/null +++ b/src/cron/expression/c/parse.c @@ -0,0 +1,179 @@ +#include "expression.h" + +const uint8_t min[4] = {0, 0, 1, 1}; +const uint8_t max[4] = {59, 23, 31, 12}; + +typedef enum parse_error { + ParseOk = 0, + ParseInvalidExpression = 1, + ParseInvalidNumber = 2, + ParseOutOfRange = 3 +} ParseError; + +#define SAFE_ATOI(v,s,min,max) \ + int _##v = atoi(s); \ + if ((_##v) == 0 && strcmp((s), "0") != 0) { \ + return ParseInvalidNumber; \ + } \ + if (v < (min) || v > (max)) { \ + return ParseOutOfRange; \ + } \ + v = (uint8_t) (_##v); + +/** + * Given a range expression, produce a bit field defining what numbers in the + * min-max range the expression represents. The first bit (starting from the + * right) corresponds to min, the max - min + 1'th bit 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 + */ +ParseError ce_parse_range(uint64_t *out, char *s, uint8_t min, uint8_t max) { + // The * expression means "every possible value" + if (s[0] == '*') { + // A '*' is only valid on its own + if (s[1] != '\0') { + return ParseInvalidExpression; + } + + *out = ~0; + + return ParseOk; + } + + size_t slash_index = 0; + size_t dash_index = 0; + size_t i = 0; + + // We first iterate over the string to determine whether it contains a slash + // and/or a dash. We know the dash can only be valid if it appears before + // the slash. + while (s[i] != '\0' && slash_index == 0) { + if (s[i] == '/') { + slash_index = i; + + s[i] = '\0'; + } else if (s[i] == '-') { + dash_index = i; + + s[i] = '\0'; + } + + i++; + } + + // Parse the three possible numbers in the pattern + uint8_t start = 0; + uint8_t end = 0; + uint8_t interval = 1; + + SAFE_ATOI(start, s, min, max); + + if (dash_index > 0) { + SAFE_ATOI(end, &s[dash_index + 1], min, max); + } + + if (slash_index > 0) { + SAFE_ATOI(interval, &s[slash_index + 1], 1, max - min); + } + + // Single number doesn't need to loop + if (end == 0 && slash_index == 0) { + *out |= 1 << (start - min); + } else { + for (;start <= end; start += interval) { + *out |= 1 << (start - min); + start += interval; + } + } + + return ParseOk; +} + +ParseError ce_parse_part(uint64_t *out, char *s, uint8_t min, uint8_t max) { + *out = 0; + + char *next; + ParseError res; + + while ((next = strchr(s, ',')) != NULL) { + next[0] = '\0'; + res = ce_parse_range(out, s, min, max); + + if (res != ParseOk) { + return res; + } + + s = next + 1; + } + + // Make sure to parse the final range as well + res = ce_parse_range(out, s, min, max); + + if (res != ParseOk) { + return res; + } + + return ParseOk; +} + +ParseError ce_parse_expression(uint64_t *out, char *s) { + uint8_t part_count = 0; + + char *next; + ParseError res; + + // Skip leading spaces + while (s[0] == ' ') { + s++; + } + + while (part_count < 4 && (next = strchr(s, ' ')) != NULL) { + next[0] = '\0'; + res = ce_parse_part(&out[part_count], s, min[part_count], max[part_count]); + + if (res != ParseOk) { + return res; + } + + s = next + 1; + size_t offset = 1; + + // Skip multiple spaces + while (next[offset] == ' ') { + offset++; + } + s = next + offset; + + part_count++; + } + + // Parse final trailing part + if (part_count < 4 && s[0] != '\0') { + // Make sure to parse the final range as well + res = ce_parse_part(&out[part_count], s, min[part_count], max[part_count]); + + if (res != ParseOk) { + return res; + } + + part_count++; + } + + // Ensure there's always 4 parts, as expressions can have between 2 and 4 parts + while (part_count < 4) { + // Expression is augmented with '*' expressions + out[part_count] = ~0; + part_count++; + } + + return ParseOk; +}