From d2b3a983e16f15d636619ebd61a3fa08c889b080 Mon Sep 17 00:00:00 2001 From: David Oberhollenzer Date: Sun, 18 Nov 2018 21:24:39 +0100 Subject: [PATCH] Initial commit Signed-off-by: David Oberhollenzer --- .gitignore | 15 +++ LICENSE | 13 ++ Makefile.am | 13 ++ README.md | 122 +++++++++++++++++ autogen.sh | 3 + configure.ac | 38 ++++++ cronscan.c | 56 ++++++++ crontab.c | 61 +++++++++ crontab/0-example | 7 + gcrond.c | 128 ++++++++++++++++++ gcrond.h | 66 +++++++++ m4/ac_define_dir.m4 | 35 +++++ m4/compiler.m4 | 40 ++++++ rdcron.c | 318 ++++++++++++++++++++++++++++++++++++++++++++ rdline.c | 77 +++++++++++ 15 files changed, 992 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile.am create mode 100644 README.md create mode 100755 autogen.sh create mode 100644 configure.ac create mode 100644 cronscan.c create mode 100644 crontab.c create mode 100644 crontab/0-example create mode 100644 gcrond.c create mode 100644 gcrond.h create mode 100644 m4/ac_define_dir.m4 create mode 100644 m4/compiler.m4 create mode 100644 rdcron.c create mode 100644 rdline.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0eaa3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.o +.deps +.dirstamp +Makefile +Makefile.in +config.* +aclocal.m4 +autom4te.cache/ +compile +configure +depcomp +install-sh +missing +stamp-h1 +gcrond diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77dfedb --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2018 David Oberhollenzer + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..6f72aea --- /dev/null +++ b/Makefile.am @@ -0,0 +1,13 @@ +ACLOCAL_AMFLAGS = -I m4 + +AM_CPPFLAGS = -D_GNU_SOURCE +AM_CFLAGS = $(WARN_CFLAGS) + +sbin_PROGRAMS = gcrond + +gcrond_SOURCES = gcrond.c gcrond.h rdcron.c crontab.c cronscan.c rdline.c + +crontabdir = @GCRONDIR@ +crontab_DATA = crontab/0-example + +EXTRA_DIST = crontab/0-example LICENSE README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..94f1b7c --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# About + +This package contains a small cron implementation called `gcrond`. + +It was written due to a perceived lack of a proper, simple cron +implementation. All other cron implementation I came across were either decade +old, abandoned pieces of horror ("Cool, I didn't even know that C syntax +allows this!") or hopelessly integrated into other, much larger projects (e.g. +absorbed by SystemD or in the case of OpenBSD cron, married to special OpenBSD +syscalls). + +It was a fun little exercise and it seems to work so far. No idea about +standards compliance tough, the implementation was mostly written against +the Wikipedia article about Cron. + +## License + +The source code in this package is provided under the OpenBSD flavored ISC +license. So you can practically do as you wish, as long as you retain the +original copyright notice. The software is provided "as is" (as usual) with +no warranty whatsoever (e.g. it might actually do what it was designed for, +but it could just as well set your carpet on fire). + +The sub directory `m4` contains third party macro files used by the build +system which may be subject to their own, respective licenses. + + +## Portability + +The program in this package has been written for and tested on a GNU/Linux +system, so there may be some GNU-isms in there in addition to Linux specific +code. Depending on your target platform, some minor porting effort may be +required. + + +# Building and installing + +This package uses autotools. If you downloaded a distribution tar ball, simply +run the `configure` script and then `make` after the Makefile has been +generated. A list of possible `configure` options can be viewed by running +`configure --help`. + +If you really wish to do so, run `make install` to install the program on your +system. + +When working with the git tree, run the `autogen.sh` script to generate the +configure script and friends. + + +# Crontab File Format + +The cron daemon reads its configuration from all files it can find +in `/etc/crontab.d/` (exact path can be configured). + +The files are read line by line. Empty lines or lines starting with '#' are +skipped. + +Each non-empty line consists of the typical cron fields: + +1. The `minute` field. Legal values are from 0 to 59. +2. The `hour` field. Legal values are from 0 to 23. +3. The `day of month` field. Legal values are from 1 to 31 (or fewer, depending + on the month. +4. The `month` field. Legal values are from 1 to 12 (January to December) + or the mnemonics `JAN`, `FEB`, `MAR`, `APR`, ... +5. The `day of week` field. Legal values are from 0 to 6 (Sunday to Saturday) + or the mnemonics `SUN`, `MON`, `TUE`, `WED`, ... +6. The command to execute. + + +The fields are separated by spaces. For the time matching fields, multiple +comma separated values can be specified (e.g. `MON,WED,FRI` for a job that +should run on Mondays, Wednesdays and Fridays). + +The wild-card character `*` matches any legal value. An stepping can be +specified by appending `/` and then a stepping (e.g. for the minute field, +`*/5` would let a job run every five minutes). + +A range of values can also be specified as `-`, for instance +`MON-FRI` would match every day from Monday to Friday (equivalent to `1-5`). + +Intervals and specific values can be combined, for instance a day of month +field `*/7,13,25` would trigger once a week, starting from the first of the +month (1,7,14,21,28), but additionally include the 13th and the 25th. The +same could be expressed as `1-31/7,13,25`. + + +Instead of specifying a terse cron matching expression, the first five fields +can be replaced with one of the following mnemonics: + +- `@yearly` or `@anually` is equivalent to `0 0 1 1 *`, i.e. 1st of January + at midnight +- `@monthly` is equivalent to `0 0 1 * *`, i.e. 1st of every month at midnight +- `@weekly` is equivalent to `0 0 * * 0`, i.e. every Sunday at midnight +- `@daily` is equivalent to `0 0 * * *`, i.e. every day at midnight +- `@hourly` is equivalent to `0 * * * *`, i.e. every first minute of the hour + +Lastly, the command field is not broken down but passed to `/bin/sh -c` +*as is*. + + +# Security Considerations + +The cron daemon currently has no means of specifying a user to run the jobs as, +so if cron runs as root, the jobs it starts do as well. Since by default it +reads its configuration from `/etc` which by default is only writable by root, +this shouldn't be too much of a problem when using cron for typical system +administration tasks. + +If a job should run as another user, tools such as `su`, `runuser`, `setpriv` +et cetera need to be used. + +# Possible Future Directions + +The following things would be nice to have: + +- decent logging for cron and the output of the jobs. +- cron jobs per user, e.g. scan `~/.crontab.d` or similar and run the collected + jobs as the respective user. +- timezone handling +- some usable strategy for handling time jumps, e.g. caused by a job that + syncs time with an NTP server on a system without RTC. diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..c08fadf --- /dev/null +++ b/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +autoreconf --force --install --symlink diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..1d9c210 --- /dev/null +++ b/configure.ac @@ -0,0 +1,38 @@ +AC_PREREQ([2.60]) + +AC_INIT([gcron], [0.1], [david.oberhollenzer@tele2.at], gcron) +AC_CONFIG_MACRO_DIR([m4]) +AM_INIT_AUTOMAKE([foreign subdir-objects dist-xz]) +AM_SILENT_RULES([yes]) +AC_PROG_CC +AC_PROG_CC_C99 +AC_PROG_INSTALL +AC_PROG_RANLIB + +UL_WARN_ADD([-Wall]) +UL_WARN_ADD([-Wextra]) +UL_WARN_ADD([-Wunused]) +UL_WARN_ADD([-Wmissing-prototypes]) +UL_WARN_ADD([-Wmissing-declarations]) +UL_WARN_ADD([-Wwrite-strings]) +UL_WARN_ADD([-Wjump-misses-init]) +UL_WARN_ADD([-Wuninitialized]) +UL_WARN_ADD([-Winit-self]) +UL_WARN_ADD([-Wlogical-op]) +UL_WARN_ADD([-Wunused-but-set-parameter]) +UL_WARN_ADD([-Wunused-but-set-variable]) +UL_WARN_ADD([-Wunused-parameter]) +UL_WARN_ADD([-Wunused-result]) +UL_WARN_ADD([-Wunused-variable]) +UL_WARN_ADD([-Wduplicated-cond]) +UL_WARN_ADD([-Wduplicated-branches]) +UL_WARN_ADD([-Wrestrict]) +UL_WARN_ADD([-Wnull-dereference]) +UL_WARN_ADD([-pedantic]) + +AC_SUBST([WARN_CFLAGS]) + +AC_CONFIG_HEADERS([config.h]) +AC_DEFINE_DIR(GCRONDIR, sysconfdir/crontab.d, [crontab source directory]) + +AC_OUTPUT([Makefile]) diff --git a/cronscan.c b/cronscan.c new file mode 100644 index 0000000..16ebb09 --- /dev/null +++ b/cronscan.c @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +int cronscan(const char *directory, crontab_t **list) +{ + crontab_t *cron, *tail = NULL; + struct dirent *ent; + int dfd, ret = 0; + DIR *dir; + + dir = opendir(directory); + if (dir == NULL) { + perror(directory); + return -1; + } + + dfd = dirfd(dir); + if (dfd < 0) { + perror(directory); + closedir(dir); + return -1; + } + + for (;;) { + errno = 0; + ent = readdir(dir); + + if (ent == NULL) { + if (errno != 0) { + perror(directory); + ret = -1; + } + break; + } + + if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, "..")) + continue; + + cron = rdcron(dfd, ent->d_name); + if (cron == NULL) + continue; + + if (tail == NULL) { + *list = cron; + tail = cron; + } else { + tail->next = cron; + } + + while (tail->next != NULL) + tail = tail->next; + } + + closedir(dir); + return ret; +} diff --git a/crontab.c b/crontab.c new file mode 100644 index 0000000..2b26ebf --- /dev/null +++ b/crontab.c @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +void cron_tm_to_mask(crontab_t *out, struct tm *t) +{ + memset(out, 0, sizeof(*out)); + out->minute = 1UL << ((unsigned long)t->tm_min); + out->hour = 1 << t->tm_hour; + out->dayofmonth = 1 << (t->tm_mday - 1); + out->month = 1 << t->tm_mon; + out->dayofweek = 1 << t->tm_wday; +} + +bool cron_should_run(const crontab_t *t, const crontab_t *mask) +{ + if ((t->minute & mask->minute) == 0) + return false; + + if ((t->hour & mask->hour) == 0) + return false; + + if ((t->dayofmonth & mask->dayofmonth) == 0) + return false; + + if ((t->month & mask->month) == 0) + return false; + + if ((t->dayofweek & mask->dayofweek) == 0) + return false; + + return true; +} + +void delcron(crontab_t *cron) +{ + if (cron != NULL) { + free(cron->exec); + free(cron); + } +} + +int runjob(crontab_t *tab) +{ + pid_t pid; + + if (tab->exec == NULL) + return 0; + + pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } + + if (pid != 0) + return 0; + + execl("/bin/sh", "sh", "-c", tab->exec, (char *) 0); + perror("runnig shell interpreter"); + exit(EXIT_FAILURE); +} diff --git a/crontab/0-example b/crontab/0-example new file mode 100644 index 0000000..fcca906 --- /dev/null +++ b/crontab/0-example @@ -0,0 +1,7 @@ +# +-------- minute (0 - 59) +# | +------ hour (0 - 23) +# | | +---- day of month (1 - 31) +# | | | +-- month (1 - 12) +# | | | | +-- day of week (0 - 6) +# | | | | | +# * * * * * command to execute diff --git a/gcrond.c b/gcrond.c new file mode 100644 index 0000000..0e28277 --- /dev/null +++ b/gcrond.c @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +static crontab_t *jobs; +static sig_atomic_t run = 1; +static sig_atomic_t rescan = 1; + +static void read_config(void) +{ + if (cronscan(GCRONDIR, &jobs)) { + fputs("Error reading configuration. Continuing anyway.\n", + stderr); + } +} + +static void cleanup_config(void) +{ + crontab_t *t; + + while (jobs != NULL) { + t = jobs; + jobs = jobs->next; + delcron(t); + } +} + +static int timeout_minutes(int minutes) +{ + time_t now = time(NULL); + struct tm t; + + localtime_r(&now, &t); + return minutes * 60 + 30 - t.tm_sec; +} + +static int calc_timeout(void) +{ + time_t now = time(NULL), future; + struct tm tmstruct; + crontab_t mask, *t; + int minutes; + + for (minutes = 0; minutes < 120; ++minutes) { + future = now + minutes * 60; + + localtime_r(&future, &tmstruct); + cron_tm_to_mask(&mask, &tmstruct); + + for (t = jobs; t != NULL; t = t->next) { + if (cron_should_run(t, &mask)) + goto out; + } + } +out: + return timeout_minutes(minutes ? minutes : 1); +} + +static void runjobs(void) +{ + time_t now = time(NULL); + struct tm tmstruct; + crontab_t mask, *t; + + localtime_r(&now, &tmstruct); + cron_tm_to_mask(&mask, &tmstruct); + + for (t = jobs; t != NULL; t = t->next) { + if (cron_should_run(t, &mask)) + runjob(t); + } +} + +static void sighandler(int signo) +{ + pid_t pid; + + switch (signo) { + case SIGINT: + case SIGTERM: + run = 0; + break; + case SIGHUP: + rescan = 1; + break; + case SIGCHLD: + while ((pid = waitpid(-1, NULL, WNOHANG)) != -1) + ; + break; + } +} + +int main(void) +{ + struct timespec stime; + struct sigaction act; + int timeout; + + memset(&act, 0, sizeof(act)); + act.sa_handler = sighandler; + sigaction(SIGINT, &act, NULL); + sigaction(SIGTERM, &act, NULL); + sigaction(SIGHUP, &act, NULL); + sigaction(SIGCHLD, &act, NULL); + + while (run) { + if (rescan == 1) { + cleanup_config(); + read_config(); + timeout = timeout_minutes(1); + rescan = 0; + } else { + runjobs(); + timeout = calc_timeout(); + } + + stime.tv_sec = timeout; + stime.tv_nsec = 0; + + while (nanosleep(&stime, &stime) != 0 && run && !rescan) { + if (errno != EINTR) { + perror("nanosleep"); + break; + } + } + } + + return EXIT_SUCCESS; +} diff --git a/gcrond.h b/gcrond.h new file mode 100644 index 0000000..c6f3071 --- /dev/null +++ b/gcrond.h @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: ISC */ +#ifndef GCROND_H +#define GCROND_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +typedef struct crontab_t { + struct crontab_t *next; + char *exec; + + uint64_t minute; + uint32_t hour; + uint32_t dayofmonth; + uint16_t month; + uint8_t dayofweek; +} crontab_t; + +typedef struct { + const char *filename; /* input file name */ + size_t lineno; /* current line number */ + FILE *fp; + char *line; +} rdline_t; + +int rdline_init(rdline_t *t, int dirfd, const char *filename); + +void rdline_complain(rdline_t *t, const char *msg, ...); + +void rdline_cleanup(rdline_t *t); + +int rdline(rdline_t *t); + +crontab_t *rdcron(int dirfd, const char *filename); + +void delcron(crontab_t *cron); + +int cronscan(const char *directory, crontab_t **list); + +void cron_tm_to_mask(crontab_t *out, struct tm *t); + +bool cron_should_run(const crontab_t *t, const crontab_t *mask); + +int runjob(crontab_t *tab); + +#endif /* GCROND_H */ diff --git a/m4/ac_define_dir.m4 b/m4/ac_define_dir.m4 new file mode 100644 index 0000000..3b48c8b --- /dev/null +++ b/m4/ac_define_dir.m4 @@ -0,0 +1,35 @@ +dnl @synopsis AC_DEFINE_DIR(VARNAME, DIR [, DESCRIPTION]) +dnl +dnl This macro sets VARNAME to the expansion of the DIR variable, +dnl taking care of fixing up ${prefix} and such. +dnl +dnl VARNAME is then offered as both an output variable and a C +dnl preprocessor symbol. +dnl +dnl Example: +dnl +dnl AC_DEFINE_DIR([DATADIR], [datadir], [Where data are placed to.]) +dnl +dnl @category Misc +dnl @author Stepan Kasal +dnl @author Andreas Schwab +dnl @author Guido U. Draheim +dnl @author Alexandre Oliva +dnl @version 2006-10-13 +dnl @license AllPermissive + +AC_DEFUN([AC_DEFINE_DIR], [ + prefix_NONE= + exec_prefix_NONE= + test "x$prefix" = xNONE && prefix_NONE=yes && prefix=$ac_default_prefix + test "x$exec_prefix" = xNONE && exec_prefix_NONE=yes && exec_prefix=$prefix +dnl In Autoconf 2.60, ${datadir} refers to ${datarootdir}, which in turn +dnl refers to ${prefix}. Thus we have to use `eval' twice. + eval ac_define_dir="\"[$]$2\"" + eval ac_define_dir="\"$ac_define_dir\"" + AC_SUBST($1, "$ac_define_dir") + AC_DEFINE_UNQUOTED($1, "$ac_define_dir", [$3]) + test "$prefix_NONE" && prefix=NONE + test "$exec_prefix_NONE" && exec_prefix=NONE +]) + diff --git a/m4/compiler.m4 b/m4/compiler.m4 new file mode 100644 index 0000000..058c73f --- /dev/null +++ b/m4/compiler.m4 @@ -0,0 +1,40 @@ +dnl Copyright (C) 2008-2011 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Simon Josefsson +dnl -- derivated from coreutils m4/warnings.m4 + +# UL_AS_VAR_APPEND(VAR, VALUE) +# ---------------------------- +# Provide the functionality of AS_VAR_APPEND if Autoconf does not have it. +m4_ifdef([AS_VAR_APPEND], +[m4_copy([AS_VAR_APPEND], [UL_AS_VAR_APPEND])], +[m4_define([UL_AS_VAR_APPEND], +[AS_VAR_SET([$1], [AS_VAR_GET([$1])$2])])]) + +# UL_ADD_WARN(COMPILER_OPTION [, VARNAME]) +# ------------------------ +# Adds parameter to WARN_CFLAGS (or to $VARNAME) if the compiler supports it. +AC_DEFUN([UL_WARN_ADD], [ + m4_define([warnvarname], m4_default([$2],WARN_CFLAGS)) + AS_VAR_PUSHDEF([ul_Warn], [ul_cv_warn_$1])dnl + AC_CACHE_CHECK([whether compiler handles $1], m4_defn([ul_Warn]), [ + # store AC_LANG_WERROR status, then turn it on + save_ac_[]_AC_LANG_ABBREV[]_werror_flag="${ac_[]_AC_LANG_ABBREV[]_werror_flag}" + AC_LANG_WERROR + + ul_save_CPPFLAGS="$CPPFLAGS" + CPPFLAGS="-Werror ${CPPFLAGS} $1" + AC_PREPROC_IFELSE([AC_LANG_PROGRAM([])], + [AS_VAR_SET(ul_Warn, [yes])], + [AS_VAR_SET(ul_Warn, [no])]) + # restore AC_LANG_WERROR + ac_[]_AC_LANG_ABBREV[]_werror_flag="${save_ac_[]_AC_LANG_ABBREV[]_werror_flag}" + + CPPFLAGS="$ul_save_CPPFLAGS" + ]) + AS_VAR_IF(ul_Warn, [yes], [UL_AS_VAR_APPEND(warnvarname, [" $1"])]) +]) + diff --git a/rdcron.c b/rdcron.c new file mode 100644 index 0000000..7e39763 --- /dev/null +++ b/rdcron.c @@ -0,0 +1,318 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +typedef struct { + const char *name; + int value; +} enum_map_t; + +static const enum_map_t weekday[] = { + { "MON", 1 }, + { "TUE", 2 }, + { "WED", 3 }, + { "THU", 4 }, + { "FRI", 5 }, + { "SAT", 6 }, + { "SUN", 0 }, + { NULL, 0 }, +}; + +static const enum_map_t month[] = { + { "JAN", 1 }, + { "FEB", 2 }, + { "MAR", 3 }, + { "APR", 4 }, + { "MAY", 5 }, + { "JUN", 6 }, + { "JUL", 7 }, + { "AUG", 8 }, + { "SEP", 9 }, + { "OCT", 10 }, + { "NOV", 11 }, + { "DEC", 12 }, + { NULL, 0 }, +}; + +static const struct { + const char *macro; + crontab_t tab; +} intervals[] = { + { + .macro = "yearly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0x01, + .dayofweek = 0xFF + }, + }, { + .macro = "annually", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0x01, + .dayofweek = 0xFF + }, + }, { + .macro = "monthly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0x01, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, { + .macro = "weekly", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0x01 + }, + }, { + .macro = "daily", + .tab = { + .minute = 0x01, + .hour = 0x01, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, { + .macro = "hourly", + .tab = { + .minute = 0x01, + .hour = 0xFFFFFFFF, + .dayofmonth = 0xFFFFFFFF, + .month = 0xFFFF, + .dayofweek = 0xFF + }, + }, +}; + +/*****************************************************************************/ + +static char *readnum(char *line, int *out, int minval, int maxval, + const enum_map_t *mnemonic, rdline_t *rd) +{ + int i, temp, value = 0; + const enum_map_t *ev; + + if (!isdigit(*line)) { + if (!mnemonic) + goto fail_mn; + + for (i = 0; isalnum(line[i]); ++i) + ; + if (i == 0) + goto fail_mn; + + temp = line[i]; + line[i] = '\0'; + + for (ev = mnemonic; ev->name != NULL; ++ev) { + if (!strcmp(line, mnemonic->name) == 0) + break; + } + + if (ev->name == NULL) { + rdline_complain(rd, "unexpected '%s'", line); + return NULL; + } + line[i] = temp; + *out = ev->value; + return line + i; + } + + while (isdigit(*line)) { + i = ((*(line++)) - '0'); + if (value > (maxval - i) / 10) + goto fail_of; + value = value * 10 + i; + } + + if (value < minval) + goto fail_uf; + + *out = value; + return line; +fail_of: + rdline_complain(rd, "value exceeds maximum (%d > %d)", value, maxval); + return NULL; +fail_uf: + rdline_complain(rd, "value too small (%d < %d)", value, minval); + return NULL; +fail_mn: + rdline_complain(rd, "expected numeric value"); + return NULL; +} + +static char *readfield(char *line, uint64_t *out, int minval, int maxval, + const enum_map_t *mnemonic, rdline_t *rd) +{ + int value, endvalue, step; + uint64_t v = 0; +next: + if (*line == '*') { + ++line; + value = minval; + endvalue = maxval; + } else { + line = readnum(line, &value, minval, maxval, mnemonic, rd); + if (!line) + goto fail; + + if (*line == '-') { + line = readnum(line + 1, &endvalue, minval, maxval, + mnemonic, rd); + if (!line) + goto fail; + } else { + endvalue = value; + } + + if (endvalue < value) + goto fail; + } + + if (*line == '/') { + line = readnum(line + 1, &step, 1, maxval + 1, NULL, rd); + if (!line) + goto fail; + } else { + step = 1; + } + + while (value <= endvalue) { + v |= 1UL << (unsigned long)(value - minval); + value += step; + } + + if (*line == ',') { + ++line; + goto next; + } + + if (*line != '\0' && !isspace(*line)) + goto fail; + while (isspace(*line)) + ++line; + + *out = v; + return line; +fail: + rdline_complain(rd, "invalid time range expression"); + return NULL; +} + +/*****************************************************************************/ + +static char *cron_interval(crontab_t *cron, rdline_t *rd) +{ + char *arg = rd->line; + size_t i, j; + + if (*(arg++) != '@') + goto fail; + for (j = 0; isalpha(arg[j]); ++j) + ; + if (j == 0 || !isspace(arg[j])) + goto fail; + + for (i = 0; i < ARRAY_SIZE(intervals); ++i) { + if (strlen(intervals[i].macro) != j) + continue; + if (strncmp(intervals[i].macro, arg, j) == 0) + break; + } + + if (i == ARRAY_SIZE(intervals)) + goto fail; + + cron->minute = intervals[i].tab.minute; + cron->hour = intervals[i].tab.hour; + cron->dayofmonth = intervals[i].tab.dayofmonth; + cron->month = intervals[i].tab.month; + cron->dayofweek = intervals[i].tab.dayofweek; + return arg + j; +fail: + rdline_complain(rd, "unknown interval '%s'", arg); + return NULL; +} + +static char *cron_fields(crontab_t *cron, rdline_t *rd) +{ + char *arg = rd->line; + uint64_t value; + + if ((arg = readfield(arg, &value, 0, 59, NULL, rd)) == NULL) + return NULL; + cron->minute = value; + + if ((arg = readfield(arg, &value, 0, 23, NULL, rd)) == NULL) + return NULL; + cron->hour = value; + + if ((arg = readfield(arg, &value, 1, 31, NULL, rd)) == NULL) + return NULL; + cron->dayofmonth = value; + + if ((arg = readfield(arg, &value, 1, 12, month, rd)) == NULL) + return NULL; + cron->month = value; + + if ((arg = readfield(arg, &value, 0, 6, weekday, rd)) == NULL) + return NULL; + cron->dayofweek = value; + + return arg; +} + +crontab_t *rdcron(int dirfd, const char *filename) +{ + crontab_t *cron, *list = NULL; + rdline_t rd; + char *ptr; + + if (rdline_init(&rd, dirfd, filename)) + return NULL; + + while (rdline(&rd) == 0) { + cron = calloc(1, sizeof(*cron)); + if (cron == NULL) { + rdline_complain(&rd, strerror(errno)); + break; + } + + if (rd.line[0] == '@') { + ptr = cron_interval(cron, &rd); + } else { + ptr = cron_fields(cron, &rd); + } + + if (ptr == NULL) { + free(cron); + continue; + } + + while (isspace(*ptr)) + ++ptr; + + cron->exec = strdup(ptr); + if (cron->exec == NULL) { + rdline_complain(&rd, strerror(errno)); + free(cron); + continue; + } + + cron->next = list; + list = cron; + } + + rdline_cleanup(&rd); + return list; +} diff --git a/rdline.c b/rdline.c new file mode 100644 index 0000000..99b4f4e --- /dev/null +++ b/rdline.c @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: ISC */ +#include "gcrond.h" + +int rdline(rdline_t *t) +{ + size_t i, len; + + do { + free(t->line); + t->line = NULL; + errno = 0; + len = 0; + + if (getline(&t->line, &len, t->fp) < 0) { + if (errno) { + rdline_complain(t, strerror(errno)); + return -1; + } + return 1; + } + + t->lineno += 1; + + for (i = 0; isspace(t->line[i]); ++i) + ; + + if (t->line[i] == '\0' || t->line[i] == '#') { + t->line[0] = '\0'; + } else if (i) { + memmove(t->line, t->line + i, len - i + 1); + } + } while (t->line[0] == '\0'); + + return 0; +} + +void rdline_complain(rdline_t *t, const char *msg, ...) +{ + va_list ap; + + fprintf(stderr, "%s: %zu: ", t->filename, t->lineno); + + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + + fputc('\n', stderr); +} + +int rdline_init(rdline_t *t, int dirfd, const char *filename) +{ + int fd; + + memset(t, 0, sizeof(*t)); + + fd = openat(dirfd, filename, O_RDONLY); + if (fd == -1) { + perror(filename); + return -1; + } + + t->fp = fdopen(fd, "r"); + if (t->fp == NULL) { + perror("fdopen"); + close(fd); + return -1; + } + + t->filename = filename; + return 0; +} + +void rdline_cleanup(rdline_t *t) +{ + free(t->line); + fclose(t->fp); +}