#include "calendar.h" #include "err.h" #include "opt.h" #include #include #include #include #define ERRP(...) \ do { \ fprintf(stderr, "err: %zu:%zu: ", s->last_line, s->last_col); \ fprintf(stderr, __VA_ARGS__); \ fprintf(stderr, "\n"); \ } while (0) struct state_s { bool err_flag; size_t line, col; size_t last_line, last_col; struct entry_interval_s cur_warn; struct entry_interval_s cur_stay; bool cur_urgent; bool cur_local; FILE *f; struct calendar_s *cal; }; enum cmd_type_e { CMD_TYPE_WARN, CMD_TYPE_URGENT, CMD_TYPE_LOCAL, CMD_TYPE_STAY, }; struct cmd_s { enum cmd_type_e type; struct entry_interval_s iv; bool b; }; struct pos_s { size_t line, col; }; static struct pos_s sub_get_pos(struct state_s *s) { struct pos_s p = { .line = s->line, .col = s->col }; return p; } static void sub_set_pos(struct state_s *s, struct pos_s p) { s->last_line = p.line; s->last_col = p.col; } static void sub_eat_spaces(struct state_s *s) { int c; while (true) { c = getc(s->f); if (c == '\n') { s->col = 1; s->line++; continue; } if (!isspace(c)) { ungetc(c, s->f); break; } s->col++; } } static void sub_eat_comment(struct state_s *s) { int c; while (true) { c = getc(s->f); if (c == '\n' || c == EOF) break; } s->col = 1; s->line++; } static struct entry_interval_s sub_get_interval(struct state_s *s) { struct entry_interval_s iv = { 0 }; int r, count; long l; bool ready = false; sub_set_pos(s, sub_get_pos(s)); while (true) { errno = 0; r = fscanf(s->f, "%ld%n", &l, &count); /* read err */ if (errno) { ERRP("invalid time interval"); s->err_flag = true; return iv; } /* no int found */ if (r == 0) { /* err if digit */ r = getc(s->f); if (isdigit(r)) { ERRP("expected time interval"); s->err_flag = true; return iv; } ungetc(r, s->f); /* err if empty so far */ if (ready == false) { ERRP("expected time interval"); s->err_flag = true; } /* done */ return iv; } /* range err interval */ if (l <= 0) { ERRP("invalid time interval"); s->err_flag = true; return iv; } if (l > INT_MAX - 365) { ERRP("time interval too large"); s->err_flag = true; return iv; } s->col += count; switch (getc(s->f)) { case 'y': if (iv.year != 0) { ERRP("duplicate year entry in time interval"); s->err_flag = true; return iv; } iv.year = l; break; case 'm': if (iv.month != 0) { ERRP("duplicate month entry in time interval"); s->err_flag = true; return iv; } iv.month = l; break; case 'd': if (iv.day != 0) { ERRP("duplicate day entry in time interval"); s->err_flag = true; return iv; } iv.day = l; break; case 'H': if (iv.hour != 0) { ERRP("duplicate hour entry in time interval"); s->err_flag = true; return iv; } iv.hour = l; break; case 'M': if (iv.minute != 0) { ERRP("duplicate minute entry in time interval"); s->err_flag = true; return iv; } iv.minute = l; break; case 'S': if (iv.second != 0) { ERRP("duplicate second entry in time interval"); s->err_flag = true; return iv; } iv.second = l; break; default: ERRP("invalid time interval"); s->err_flag = true; return iv; } s->col++; ready = true; sub_eat_spaces(s); } } static time_t sub_get_date(struct state_s *s) { int r, count; long l; int i; char c; time_t rt = 0; struct tm ttmp; struct tm t = { .tm_year = 2000, .tm_mday = 1, .tm_isdst = -1, }; bool seen = false; sub_set_pos(s, sub_get_pos(s)); for (i = 0; i < 6; i++) { c = getc(s->f); if (c == '_') { count = 1; switch (i) { /* year */ case 0: l = 2000; break; case 1: case 2: l = 1; break; default: l = 0; break; } goto skip_num; } ungetc(c, s->f); errno = 0; r = fscanf(s->f, "%ld%n", &l, &count); /* read err */ if (errno) { ERRP("invalid date"); s->err_flag = true; return rt; } /* no int found */ if (r == 0) { if (seen) ERRP("invalid date"); else ERRP("expected date"); s->err_flag = true; return rt; } seen = true; /* range err interval */ if (l < 0) { ERRP("invalid date"); s->err_flag = true; return rt; } if (l > INT_MAX) { ERRP("invalid date"); s->err_flag = true; return rt; } skip_num: switch (i) { case 0: t.tm_year = l - 1900; break; case 1: t.tm_mon = l - 1; break; case 2: t.tm_mday = l; break; case 3: t.tm_hour = l; break; case 4: t.tm_min = l; break; case 5: t.tm_sec = l; break; } s->col += count; if (i < 2) { c = getc(s->f); if (c != '-') { ungetc(c, s->f); goto validate; } s->col++; } else if (i == 2) { sub_eat_spaces(s); c = getc(s->f); ungetc(c, s->f); if (!isdigit(c) && c != '_') goto validate; } else if (i < 5) { c = getc(s->f); if (c != ':') { ungetc(c, s->f); goto validate; } } } validate: ttmp = t; /* NOTE: timegm is non-POSIX, but commonplace */ if (s->cur_local) rt = mktime(&t); else rt = timegm(&t); if (rt == -1) { ERRP("invalid date"); s->err_flag = true; } if (s->cur_local) t = *localtime(&rt); else t = *gmtime(&rt); if ( ttmp.tm_year != t.tm_year || ttmp.tm_mon != t.tm_mon || ttmp.tm_mday != t.tm_mday || ttmp.tm_hour != t.tm_hour || ttmp.tm_min != t.tm_min || ttmp.tm_sec != t.tm_sec ) { ERRP("invalid date"); s->err_flag = true; } return rt; } static inline struct cmd_s sub_get_cmd_warn(struct state_s *s) { struct cmd_s cmd = { .type = CMD_TYPE_WARN }; cmd.iv = sub_get_interval(s); return cmd; } static inline struct cmd_s sub_get_cmd_stay(struct state_s *s) { struct cmd_s cmd = { .type = CMD_TYPE_STAY }; cmd.iv = sub_get_interval(s); return cmd; } static bool sub_get_bool(struct state_s *s) { bool r = false; sub_set_pos(s, sub_get_pos(s)); switch (tolower(getc(s->f))) { case 't': if ( tolower(getc(s->f)) == 'r' && tolower(getc(s->f)) == 'u' && tolower(getc(s->f)) == 'e' ) { r = true; s->col += 4; } else { s->err_flag = true; } break; case 'y': if ( tolower(getc(s->f)) == 'e' && tolower(getc(s->f)) == 's' ) { r = true; s->col += 3; } else { s->err_flag = true; } break; case 'n': if ( tolower(getc(s->f)) == 'o' ) { s->col += 2; } else { s->err_flag = true; } break; case 'f': if ( tolower(getc(s->f)) == 'a' && tolower(getc(s->f)) == 'l' && tolower(getc(s->f)) == 's' && tolower(getc(s->f)) == 'e' ) { s->col += 5; } else { s->err_flag = true; } break; case 'o': switch (tolower(getc(s->f))) { case 'n': r = true; s->col += 2; break; case 'f': if (tolower(getc(s->f)) == 'f') { s->col += 3; } else { s->err_flag = true; } break; default: s->err_flag = true; break; } break; default: s->err_flag = true; break; } if (s->err_flag) ERRP("invalid boolean"); return r; } static inline struct cmd_s sub_get_cmd_urgent(struct state_s *s) { struct cmd_s cmd = { .type = CMD_TYPE_URGENT }; cmd.b = sub_get_bool(s); return cmd; } static inline struct cmd_s sub_get_cmd_local(struct state_s *s) { struct cmd_s cmd = { .type = CMD_TYPE_LOCAL }; cmd.b = sub_get_bool(s); return cmd; } static inline struct cmd_s sub_get_cmd(struct state_s *s) { struct cmd_s cmd = { 0 }; struct pos_s p = { .line = s->line, .col = s->col - 1, }; sub_set_pos(s, p); switch (getc(s->f)) { case 'w': if ( getc(s->f) == 'a' && getc(s->f) == 'r' && getc(s->f) == 'n' ) { s->col += 4; sub_eat_spaces(s); return sub_get_cmd_warn(s); } break; case 'u': if ( getc(s->f) == 'r' && getc(s->f) == 'g' && getc(s->f) == 'e' && getc(s->f) == 'n' && getc(s->f) == 't' ) { s->col += 6; sub_eat_spaces(s); return sub_get_cmd_urgent(s); } break; case 'l': if ( getc(s->f) == 'o' && getc(s->f) == 'c' && getc(s->f) == 'a' && getc(s->f) == 'l' ) { s->col += 5; sub_eat_spaces(s); return sub_get_cmd_local(s); } break; case 's': if ( getc(s->f) == 't' && getc(s->f) == 'a' && getc(s->f) == 'y' ) { s->col += 4; sub_eat_spaces(s); return sub_get_cmd_stay(s); } break; } ERRP("unknown command"); s->err_flag = true; return cmd; } static char* sub_get_msg(struct state_s *s) { const size_t step_size = 64; size_t count = 0, width = 0; char *msg = NULL; char c; while ( (c = getc(s->f)) != '\n' && c != EOF) { s->col++; if (count == width || count + 1 == width) { width += step_size; msg = realloc(msg, width * sizeof(*msg)); if (msg == NULL) { ERRM("failed to allocate memory"); s->err_flag = true; return NULL; } } if (c == opt_line_delim() || c == opt_col_delim()) { sub_set_pos(s, sub_get_pos(s)); ERRP("message may not contain line or column delimiter characters"); s->err_flag = true; free(msg); return NULL; } msg[count] = c; count++; } msg[count] = '\0'; s->col = 0; s->line++; return msg; } static inline struct entry_s sub_get_entry_on(struct state_s *s) { struct entry_s e = { .type = ENTRY_TYPE_ON, .urgent = s->cur_urgent, .local = s->cur_local, .warn = s->cur_warn, .stay = s->cur_stay, }; time_t rt; int c; rt = sub_get_date(s); if (s->err_flag) return e; e.start = rt; sub_eat_spaces(s); c = getc(s->f); if (c != ',') { ungetc(c, s->f); return e; } s->col++; sub_eat_spaces(s); e.msg = sub_get_msg(s); return e; } static inline struct entry_s sub_get_entry_every(struct state_s *s) { struct entry_s e = { .type = ENTRY_TYPE_EVERY, .urgent = s->cur_urgent, .local = s->cur_local, .warn = s->cur_warn, .stay = s->cur_stay, }; bool from_seen = false, to_seen = false; char c; struct pos_s p = { .line = s->last_line, .col = s->last_col, }; e.every = sub_get_interval(s); if (s->err_flag) return e; while (true) { sub_eat_spaces(s); sub_set_pos(s, p); switch (c = getc(s->f)) { case 'f': if ( getc(s->f) == 'r' && getc(s->f) == 'o' && getc(s->f) == 'm' ) { if (from_seen) { ERRP("invalid calendar entry"); s->err_flag = true; return e; } from_seen = true; s->col += 4; sub_eat_spaces(s); e.start = sub_get_date(s); if (s->err_flag) return e; break; } case 't': if ( getc(s->f) == 'o' ) { if (to_seen) { ERRP("invalid calendar entry"); s->err_flag = true; return e; } to_seen = true; s->col += 2; sub_eat_spaces(s); e.end = sub_get_date(s); e.has_end = true; if (s->err_flag) return e; break; } default: goto done; } } done: if (!from_seen) { ERRP("invalid calendar entry"); s->err_flag = true; } ungetc(c, s->f); sub_eat_spaces(s); c = getc(s->f); if (c != ',') { ungetc(c, s->f); return e; } s->col++; sub_eat_spaces(s); e.msg = sub_get_msg(s); return e; } static inline struct entry_s sub_get_entry(struct state_s *s) { struct entry_s e = { 0 }; switch (getc(s->f)) { case 'o': if ( getc(s->f) == 'n' ) { s->col += 2; sub_eat_spaces(s); return sub_get_entry_on(s); } break; case 'e': if ( getc(s->f) == 'v' && getc(s->f) == 'e' && getc(s->f) == 'r' && getc(s->f) == 'y' ) { s->col += 5; sub_eat_spaces(s); return sub_get_entry_every(s); } break; } ERRP("invalid calendar entry"); s->err_flag = true; return e; } static inline void sub_cal_push(struct state_s *s, struct calendar_s *cal, struct entry_s *e) { const size_t step_size = 64; if (cal->count == cal->width) { cal->width += step_size; cal->entries = realloc(cal->entries, cal->width * sizeof(*e)); if (cal->entries == NULL) { ERRM("failed to allocate memory"); s->err_flag = true; return; } } cal->entries[cal->count] = *e; cal->count++; } struct calendar_s calendar_parse(FILE *f) { struct calendar_s cal = { .count = 0, .err_flag = false, }; struct state_s s = { .err_flag = false, .line = 1, .col = 1, .cur_warn = { .day = 1, }, .cur_stay = { .day = 1, }, .cur_urgent = false, .cur_local = true, .f = f, .cal = &cal, }; int c; struct cmd_s cmd; struct entry_s e; while (true) { sub_eat_spaces(&s); sub_set_pos(&s, sub_get_pos(&s)); c = getc(f); if (c == EOF) break; if (c == '#') { sub_eat_comment(&s); continue; } if (c == '.') { s.col++; cmd = sub_get_cmd(&s); if (s.err_flag) { calendar_wipe(&cal); cal.err_flag = true; return cal; } switch (cmd.type) { case CMD_TYPE_WARN: s.cur_warn = cmd.iv; break; case CMD_TYPE_URGENT: s.cur_urgent = cmd.b; break; case CMD_TYPE_LOCAL: s.cur_local = cmd.b; break; case CMD_TYPE_STAY: s.cur_stay = cmd.iv; break; } continue; } ungetc(c, f); e = sub_get_entry(&s); if (s.err_flag) { calendar_wipe(&cal); cal.err_flag = true; return cal; } sub_cal_push(&s, &cal, &e); } return cal; } void calendar_wipe(struct calendar_s *cal) { size_t i; if (cal->entries == NULL) return; for (i = 0; i < cal->count; i++) { if (cal->entries[i].msg != NULL) free(cal->entries[i].msg); } free(cal->entries); cal->entries = NULL; }