aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Oberhollenzer <david.oberhollenzer@tele2.at>2018-11-18 21:24:39 +0100
committerDavid Oberhollenzer <david.oberhollenzer@tele2.at>2018-11-23 02:01:21 +0100
commitd2b3a983e16f15d636619ebd61a3fa08c889b080 (patch)
tree54740409709b76c7045bcb634b5b79377f7798c3
Initial commit
Signed-off-by: David Oberhollenzer <david.oberhollenzer@tele2.at>
-rw-r--r--.gitignore15
-rw-r--r--LICENSE13
-rw-r--r--Makefile.am13
-rw-r--r--README.md122
-rwxr-xr-xautogen.sh3
-rw-r--r--configure.ac38
-rw-r--r--cronscan.c56
-rw-r--r--crontab.c61
-rw-r--r--crontab/0-example7
-rw-r--r--gcrond.c128
-rw-r--r--gcrond.h66
-rw-r--r--m4/ac_define_dir.m435
-rw-r--r--m4/compiler.m440
-rw-r--r--rdcron.c318
-rw-r--r--rdline.c77
15 files changed, 992 insertions, 0 deletions
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 <david.oberhollenzer@tele2.at>
+
+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 `<lower>-<upper>`, 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 <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <limits.h>
+#include <signal.h>
+#include <dirent.h>
+#include <stdarg.h>
+#include <ctype.h>
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+
+#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 <kasal@ucw.cz>
+dnl @author Andreas Schwab <schwab@suse.de>
+dnl @author Guido U. Draheim <guidod@gmx.de>
+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);
+}