Use a custom function rather than boost::algorithm::replace_all.

Reduce dependencies and executable size.

/* crontab.rl - configure file parser for ncron
 *
 * (c) 2003-2016 Nicholas J. Kain <njkain at gmail dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <algorithm>
#include <utility>
#include <unordered_map>
#include <cstdio>
#include <format.hpp>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <assert.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <limits.h>
extern "C" {
#include "nk/privilege.h"
}

#include "ncron.hpp"
#include "sched.hpp"
#include "crontab.hpp"

/* BSD uses OFILE rather than NOFILE... */
#ifndef RLIMIT_NOFILE
#  define RLIMIT_NOFILE RLIMIT_OFILE
#endif

#define MAX_LINE 2048

static int cfg_reload;    /* 0 on first call, 1 on subsequent calls */
extern int gflags_debug;

static void get_history(std::unique_ptr<cronentry_t> &item);

struct ParseCfgState
{
    ParseCfgState(const std::string &ef, std::vector<StackItem> &stk,
                  std::vector<StackItem> &dstk) :
        stack(stk), deadstack(dstk), ce(nullptr), execfile(ef),
        jobid_st(nullptr), time_st(nullptr), intv_st(nullptr),
        intv2_st(nullptr), strv_st(nullptr), v_strlen(0), linenum(0), v_int(0),
        v_int2(0), cs(0), cmdret(0), intv2_exist(false), runat(false)
    {
        memset(v_str, 0, sizeof v_str);
    }
    char v_str[1024];

    std::vector<StackItem> &stack;
    std::vector<StackItem> &deadstack;
    std::unique_ptr<cronentry_t> ce;

    const std::string execfile;

    const char *jobid_st;
    const char *time_st;
    const char *intv_st;
    const char *intv2_st;
    const char *strv_st;

    size_t v_strlen;
    size_t linenum;

    unsigned int v_time;

    int v_int;
    int v_int2;

    int cs;
    int cmdret;

    bool intv2_exist;
    bool runat;

    void create_ce()
    {
        assert(!ce);
        ce = std::make_unique<cronentry_t>();
        cmdret = 0;
        runat = false;
    }

    inline void debug_print_ce() const
    {
        if (!gflags_debug)
            return;
        fmt::print(stderr, "-=- finish_ce -=-\n");
        fmt::print(stderr, "id: {}\n", ce->id);
        fmt::print(stderr, "command: {}\n", ce->command);
        fmt::print(stderr, "args: {}\n", ce->args);
        fmt::print(stderr, "chroot: {}\n", ce->chroot);
        fmt::print(stderr, "numruns: {}\n", ce->numruns);
        fmt::print(stderr, "maxruns: {}\n", ce->maxruns);
        fmt::print(stderr, "journal: {}\n", ce->journal);
        fmt::print(stderr, "user: {}\n", ce->user);
        fmt::print(stderr, "group: {}\n", ce->group);
        for (const auto &i: ce->month)
            fmt::print(stderr, "month: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->day)
            fmt::print(stderr, "day: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->weekday)
            fmt::print(stderr, "weekday: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->hour)
            fmt::print(stderr, "hour: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->minute)
            fmt::print(stderr, "minute: [{},{}]\n", i.first, i.second);
        fmt::print(stderr, "interval: {}\n", ce->interval);
        fmt::print(stderr, "exectime: {}\n", ce->exectime);
        fmt::print(stderr, "lasttime: {}\n", ce->lasttime);
    }

    inline void debug_print_ce_history() const
    {
        if (!gflags_debug)
            return;
        fmt::print(stderr, "[{}]->numruns = {}\n", ce->id, ce->numruns);
        fmt::print(stderr, "[{}]->exectime = {}\n", ce->id, ce->exectime);
        fmt::print(stderr, "[{}]->lasttime = {}\n", ce->id, ce->lasttime);
    }

    void finish_ce()
    {
        if (!ce)
            return;
        debug_print_ce();

        if (ce->id <= 0
            || (ce->interval <= 0 && ce->exectime <= 0)
            || ce->command.empty() || cmdret < 1) {
            if (gflags_debug)
                fmt::print(stderr, "===> IGNORE\n");
            ce.reset();
            return;
        }
        if (gflags_debug)
            fmt::print(stderr, "===> ADD\n");

        /* we have a job to insert */
        if (runat) { /* runat task */
            auto forced_exectime = ce->exectime;
            get_history(ce);
            ce->exectime = forced_exectime;
            debug_print_ce_history();

            /* insert iif we haven't exceeded maxruns */
            if (!ce->numruns)
                stack.emplace_back(std::move(ce));
            else
                deadstack.emplace_back(std::move(ce));
        } else { /* interval task */
            get_history(ce);
            debug_print_ce_history();
            set_initial_exectime(*ce);

            /* insert iif numruns < maxruns and no constr error */
            if ((ce->maxruns == 0 || ce->numruns < ce->maxruns)
                && ce->exectime != 0)
                stack.emplace_back(std::move(ce));
            else
                deadstack.emplace_back(std::move(ce));
        }
        ce.reset();
    }

    void setgroupv()
    {
        if (nk_gidbyname(v_str, &ce->group)) {
            fmt::print(stderr, "{}: nonexistent group specified at line {}\n",
                       __func__, linenum);
            std::exit(EXIT_FAILURE);
        }
    }

    void setuserv()
    {
        if (nk_uidgidbyname(v_str, &ce->user, &ce->group)) {
            fmt::print(stderr, "{}: nonexistent user specified at line {}\n",
                       __func__, linenum);
            std::exit(EXIT_FAILURE);
        }
    }

    void setlim(int type)
    {
        struct rlimit rli;
        rli.rlim_cur = v_int == 0 ? RLIM_INFINITY : v_int;
        rli.rlim_max = v_int2 == 0 ? RLIM_INFINITY : v_int2;

        if (!ce->limits)
            ce->limits = std::make_unique<rlimits>();

        switch (type) {
        case RLIMIT_CPU: ce->limits->cpu = rli; break;
        case RLIMIT_FSIZE: ce->limits->fsize = rli; break;
        case RLIMIT_DATA: ce->limits->data = rli; break;
        case RLIMIT_STACK: ce->limits->stack = rli; break;
        case RLIMIT_CORE: ce->limits->core = rli; break;
        case RLIMIT_RSS: ce->limits->rss = rli; break;
        case RLIMIT_NPROC: ce->limits->nproc = rli; break;
        case RLIMIT_NOFILE: ce->limits->nofile = rli; break;
        case RLIMIT_MEMLOCK: ce->limits->memlock = rli; break;
    #ifndef BSD
        case RLIMIT_AS: ce->limits->as = rli; break;
        case RLIMIT_MSGQUEUE: ce->limits->msgqueue = rli; break;
        case RLIMIT_NICE: ce->limits->nice = rli; break;
        case RLIMIT_RTTIME: ce->limits->rttime = rli; break;
        case RLIMIT_RTPRIO: ce->limits->rtprio = rli; break;
        case RLIMIT_SIGPENDING: ce->limits->sigpending = rli; break;
    #endif /* BSD */
        default: fmt::print(stderr, "{}: Bad RLIMIT_type specified.\n", __func__);
                 std::exit(EXIT_FAILURE);
        }
    }

};

struct item_history {
    item_history() {}
    item_history(boost::optional<time_t> e, boost::optional<time_t> l,
                 boost::optional<unsigned int> n) :
        exectime(e), lasttime(l), numruns(n) {}
    boost::optional<time_t> exectime;
    boost::optional<time_t> lasttime;
    boost::optional<unsigned int> numruns;
};

struct hstm {
    hstm() : st(nullptr), cs(0), id(0) {}
    const char *st;
    int cs;
    unsigned int id;
    item_history h;
};

%%{
    machine history_m;
    access hst.;

    action St { hst.st = p; }
    action LastTimeEn { hst.h.lasttime = atoi(hst.st); }
    action NumRunsEn { hst.h.numruns = atoi(hst.st); }
    action ExecTimeEn { hst.h.exectime = atoi(hst.st); }
    action IdEn { hst.id = atoi(hst.st); }

    lasttime = '|' digit+ > St % LastTimeEn;
    numruns = ':' digit+ > St % NumRunsEn;
    exectime = '=' digit+ > St % ExecTimeEn;
    id = digit+ > St % IdEn;
    main := id (numruns | exectime | lasttime)+;
}%%

%% write data;

static int do_parse_history(hstm &hst, const char *p, size_t plen)
{
    const char *pe = p + plen;
    const char *eof = pe;

    %% write init;
    %% write exec;

    if (hst.cs >= history_m_first_final)
        return 1;
    if (hst.cs == history_m_error)
        return -1;
    return -2;
}

static std::unordered_map<unsigned int, item_history> history_map;

static void parse_history(const std::string &path)
{
    char buf[MAX_LINE];
    auto f = fopen(path.c_str(), "r");
    if (!f) {
        fmt::print(stderr, "{}: failed to open history file \"{}\" for read: {}\n",
                   __func__, path, strerror(errno));
        return;
    }
    size_t linenum = 0;
    while (!feof(f)) {
        auto fsv = fgets(buf, sizeof buf, f);
        auto llen = strlen(buf);
        if (buf[llen-1] == '\n')
            buf[--llen] = 0;
        ++linenum;
        if (!fsv) {
            if (!feof(f))
                fmt::print(stderr, "{}: io error fetching line of '{}'\n", __func__, path);
            break;
        }
        if (llen == 0)
            continue;
        hstm h;
        auto r = do_parse_history(h, buf, llen);
        if (r < 0) {
            if (r == -2)
                fmt::print(stderr, "{}: Incomplete configuration at line {}; ignoring\n",
                           __func__, linenum);
            else
                fmt::print(stderr, "{}: Malformed configuration at line {}; ignoring.\n",
                           __func__, linenum);
            continue;
        }
        history_map.emplace(std::make_pair(
            h.id, item_history(h.h.exectime, h.h.lasttime, h.h.numruns)));
    }
    fclose(f);
}

static void get_history(std::unique_ptr<cronentry_t> &item)
{
    assert(item);

    auto i = history_map.find(item->id);
    if (i == history_map.end())
        return;
    if (i->second.exectime) {
        auto exectm = *i->second.exectime;
        item->exectime = exectm > 0 ? exectm : 0;
    }
    if (i->second.lasttime) {
        auto lasttm = *i->second.lasttime;
        item->lasttime = lasttm > 0 ? lasttm : 0;
    }
    if (i->second.numruns) {
        item->numruns = *i->second.numruns;
    }
}

static void addcstlist(ParseCfgState &ncs, cronentry_t::cst_list &list,
                       int wildcard, int min, int max)
{
    int low = ncs.v_int;
    int high = wildcard;
    if (ncs.intv2_exist)
        high = ncs.v_int2;

    if (low > max || low < min)
        low = wildcard;
    if (high > max || high < min)
        high = wildcard;

    /* we don't allow meaningless 'rules' */
    if (low == wildcard && high == wildcard)
        return;

    if (low > high) {
        /* discontinuous range, split into two continuous rules... */
        list.emplace_back(std::make_pair(low, max));
        list.emplace_back(std::make_pair(min, high));
    } else {
        /* handle continuous ranges normally */
        list.emplace_back(std::make_pair(low, high));
    }
}

static inline void string_replace_all(std::string &s, const char *from,
                                      size_t fromlen, const char *to)
{
    size_t pos{0};
    while ((pos = s.find(from, pos)) != std::string::npos) {
        s.replace(pos, fromlen, to);
        pos += fromlen;
    }
}

struct pckm {
    pckm() : st(nullptr), cs(0) {}
    char *st;
    int cs;
};

%%{
    machine parse_cmd_key_m;
    access pckm.;

    action St { pckm.st = p; }
    action CmdEn {
        ncs.ce->command = std::string(pckm.st, p - pckm.st);
        string_replace_all(ncs.ce->command, "\\ ", 2, " ");
        string_replace_all(ncs.ce->command, "\\\\", 2, "\\");
    }
    action ArgEn { ncs.ce->args = std::string(pckm.st, p - pckm.st); }

    sptab = [ \t];
    cmdstr = ([^\0 \t] | '\\\\' | '\\ ')+;
    cmd = sptab* (cmdstr > St % CmdEn);
    args = sptab+ ([^\0])* > St % ArgEn;
    main := cmd args?;
}%%

%% write data;

// cmdret = 0: Not parsed a command key yet.
// cmdret = 1: Success.  Got a command key.
// cmdret = -1: Error: malformed command key.
// cmdret = -2: Error: incomplete command key.
// cmdret = -3: Error: duplicate command key.
static void parse_command_key(ParseCfgState &ncs)
{
    char *p = ncs.v_str;
    const char *pe = ncs.v_str + ncs.v_strlen;
    const char *eof = pe;

    struct pckm pckm;

    if (ncs.cmdret != 0) {
        ncs.cmdret = -3;
        fmt::print(stderr, "Duplicate 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    }

    %% write init;
    %% write exec;

    if (pckm.cs == parse_cmd_key_m_error) {
        ncs.cmdret = -1;
        fmt::print(stderr, "Malformed 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    } else if (pckm.cs >= parse_cmd_key_m_first_final)
        ncs.cmdret = 1;
    else {
        ncs.cmdret = -2;
        fmt::print(stderr, "Incomplete 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    }
}

%%{
    machine ncrontab;
    access ncs.;

    spc = [ \t];
    eqsep = spc* '=' spc*;

    action TUnitSt { ncs.time_st = p; ncs.v_time = 0; }
    action TSecEn  { ncs.v_time +=          atoi(ncs.time_st); }
    action TMinEn  { ncs.v_time += 60     * atoi(ncs.time_st); }
    action THrEn   { ncs.v_time += 3600   * atoi(ncs.time_st); }
    action TDayEn  { ncs.v_time += 86400  * atoi(ncs.time_st); }
    action TWeekEn { ncs.v_time += 604800 * atoi(ncs.time_st); }

    action IntValSt {
        ncs.intv_st = p;
        ncs.v_int = ncs.v_int2 = 0;
        ncs.intv2_exist = false;
    }
    action IntValEn { ncs.v_int = atoi(ncs.intv_st); }
    action IntVal2St { ncs.intv2_st = p; }
    action IntVal2En {
        ncs.v_int2 = atoi(ncs.intv2_st);
        ncs.intv2_exist = true;
    }

    action StrValSt { ncs.strv_st = p; ncs.v_strlen = 0; }
    action StrValEn {
        ncs.v_strlen = p - ncs.strv_st;
        if (ncs.v_strlen <= INT_MAX) {
            ssize_t snl = snprintf(ncs.v_str, sizeof ncs.v_str,
                                   "%.*s", (int)ncs.v_strlen, ncs.strv_st);
            if (snl < 0 || (size_t)snl >= sizeof ncs.v_str) {
                fmt::print(stderr, "error parsing line {} in crontab; too long?\n",
                           ncs.linenum);
                std::exit(EXIT_FAILURE);
            }
        }
    }

    t_sec  = (digit+ > TUnitSt) 's' % TSecEn;
    t_min  = (digit+ > TUnitSt) 'm' % TMinEn;
    t_hr   = (digit+ > TUnitSt) 'h' % THrEn;
    t_day  = (digit+ > TUnitSt) 'd' % TDayEn;
    t_week = (digit+ > TUnitSt) 'w' % TWeekEn;
    t_any = (t_sec | t_min | t_hr | t_day | t_week);

    intval = (digit+ > IntValSt % IntValEn);
    timeval = t_any (spc* t_any)*;
    intrangeval = (digit+ > IntValSt % IntValEn)
                  (',' (digit+ > IntVal2St % IntVal2En))?;
    stringval = ([^\0\n]+ > StrValSt % StrValEn);

    action JournalEn { ncs.ce->journal = true; }
    journal = 'journal'i % JournalEn;

    action RunAtEn {
        ncs.runat = true;
        ncs.ce->exectime = ncs.v_int;
        ncs.ce->maxruns = 1;
        ncs.ce->journal = true;
    }
    action MaxRunsEn {
        if (!ncs.runat)
            ncs.ce->maxruns = ncs.v_int;
    }

    runat = 'runat'i eqsep intval % RunAtEn;
    maxruns = 'maxruns'i eqsep intval % MaxRunsEn;

    action LimAsEn { ncs.setlim(RLIMIT_AS); }
    action LimMemlockEn { ncs.setlim(RLIMIT_MEMLOCK); }
    action LimNofileEn { ncs.setlim(RLIMIT_NOFILE); }
    action LimNprocEn { ncs.setlim(RLIMIT_NPROC); }
    action LimRssEn { ncs.setlim(RLIMIT_RSS); }
    action LimCoreEn { ncs.setlim(RLIMIT_CORE); }
    action LimStackEn { ncs.setlim(RLIMIT_STACK); }
    action LimDataEn { ncs.setlim(RLIMIT_DATA); }
    action LimFsizeEn { ncs.setlim(RLIMIT_FSIZE); }
    action LimCpuEn { ncs.setlim(RLIMIT_CPU); }
    action LimMsgQueueEn { ncs.setlim(RLIMIT_MSGQUEUE); }
    action LimNiceEn { ncs.setlim(RLIMIT_NICE); }
    action LimRtTimeEn { ncs.setlim(RLIMIT_RTTIME); }
    action LimRtPrioEn { ncs.setlim(RLIMIT_RTPRIO); }
    action LimSigPendingEn { ncs.setlim(RLIMIT_SIGPENDING); }

    lim_as = 'l_as'i eqsep intrangeval % LimAsEn;
    lim_memlock = 'l_memlock'i eqsep intrangeval % LimMemlockEn;
    lim_nofile = 'l_nofile'i eqsep intrangeval % LimNofileEn;
    lim_nproc = 'l_nproc'i eqsep intrangeval % LimNprocEn;
    lim_rss = 'l_rss'i eqsep intrangeval % LimRssEn;
    lim_core = 'l_core'i eqsep intrangeval % LimCoreEn;
    lim_stack = 'l_stack'i eqsep intrangeval % LimStackEn;
    lim_data = 'l_data'i eqsep intrangeval % LimDataEn;
    lim_fsize = 'l_fsize'i eqsep intrangeval % LimFsizeEn;
    lim_cpu = 'l_cpu'i eqsep intrangeval % LimCpuEn;
    lim_msgqueue = 'l_msgqueue'i eqsep intrangeval % LimMsgQueueEn;
    lim_nice = 'l_nice'i eqsep intrangeval % LimNiceEn;
    lim_rttime = 'l_rttime'i eqsep intrangeval % LimRtTimeEn;
    lim_rtprio = 'l_rtprio'i eqsep intrangeval % LimRtPrioEn;
    lim_sigpending = 'l_sigpending'i eqsep intrangeval % LimSigPendingEn;

    action IntervalEn { ncs.ce->interval = ncs.v_time; }

    interval = 'interval'i eqsep timeval % IntervalEn;

    action MonthEn { addcstlist(ncs, ncs.ce->month, 0, 1, 12); }
    action DayEn { addcstlist(ncs, ncs.ce->day, 0, 1, 31); }
    action WeekdayEn { addcstlist(ncs, ncs.ce->weekday, 0, 1, 7); }
    action HourEn { addcstlist(ncs, ncs.ce->hour, 24, 0, 23); }
    action MinuteEn { addcstlist(ncs, ncs.ce->minute, 60, 0, 59); }

    month = 'month'i eqsep intrangeval % MonthEn;
    day = 'day'i eqsep intrangeval % DayEn;
    weekday = 'weekday'i eqsep intrangeval % WeekdayEn;
    hour = 'hour'i eqsep intrangeval % HourEn;
    minute = 'minute'i eqsep intrangeval % MinuteEn;

    action GroupEn { ncs.setgroupv(); }
    action UserEn { ncs.setuserv(); }
    action ChrootEn {
        ncs.ce->chroot = std::string(ncs.v_str, ncs.v_strlen);
    }
    action CommandEn { parse_command_key(ncs); }

    group = 'group'i eqsep stringval % GroupEn;
    user = 'user'i eqsep stringval % UserEn;
    chroot = 'chroot'i eqsep stringval % ChrootEn;
    command = 'command'i eqsep stringval % CommandEn;

    cmds = command | chroot | user | group | minute | hour | weekday | day |
           month | interval | lim_cpu | lim_fsize | lim_data | lim_stack |
           lim_core | lim_rss | lim_nproc | lim_nofile | lim_memlock | lim_as |
           lim_msgqueue | lim_nice | lim_rttime | lim_rtprio | lim_sigpending |
           maxruns | runat | journal;

    action JobIdSt { ncs.jobid_st = p; }
    action JobIdEn { ncs.ce->id = atoi(ncs.jobid_st); }
    action CreateCe { ncs.finish_ce(); ncs.create_ce(); }

    jobid = ('!' > CreateCe) (digit+ > JobIdSt) % JobIdEn;

    main := jobid | cmds;
}%%

%% write data;

static int do_parse_config(ParseCfgState &ncs, const char *p, size_t plen)
{
    const char *pe = p + plen;
    const char *eof = pe;

    %% write init;
    %% write exec;

    if (ncs.cs == ncrontab_error)
        return -1;
    if (ncs.cs >= ncrontab_first_final)
        return 1;
    return 0;
}

void parse_config(const std::string &path, const std::string &execfile,
                  std::vector<StackItem> &stk,
                  std::vector<StackItem> &deadstk)
{
    struct ParseCfgState ncs(execfile, stk, deadstk);
    parse_history(ncs.execfile);

    char buf[MAX_LINE];
    auto f = fopen(path.c_str(), "r");
    if (!f) {
        fmt::print(stderr, "{}: failed to open file: '{}': {}\n",
                   __func__, path, strerror(errno));
        std::exit(EXIT_FAILURE);
    }
    while (!feof(f)) {
        auto fsv = fgets(buf, sizeof buf, f);
        auto llen = strlen(buf);
        if (buf[llen-1] == '\n')
            buf[--llen] = 0;
        ++ncs.linenum;
        if (!fsv) {
            if (!feof(f))
                fmt::print(stderr, "{}: io error fetching line of '{}'\n", __func__, path);
            break;
        }
        if (llen == 0)
            continue;
        auto r = do_parse_config(ncs, buf, llen);
        if (r < 0) {
            fmt::print(stderr, "{}: do_parse_config({}) failed at line {}\n",
                       __func__, path, ncs.linenum);
            std::exit(EXIT_FAILURE);
        }
    }
    std::make_heap(stk.begin(), stk.end(), GtCronEntry);
    history_map.clear();
    cfg_reload = 1;
    fclose(f);
}

/* crontab.rl - configure file parser for ncron
 *
 * (c) 2003-2016 Nicholas J. Kain <njkain at gmail dot com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <algorithm>
#include <utility>
#include <unordered_map>
#include <boost/algorithm/string/replace.hpp>
#include <cstdio>
#include <format.hpp>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <assert.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <limits.h>
extern "C" {
#include "nk/privilege.h"
}

#include "ncron.hpp"
#include "sched.hpp"
#include "crontab.hpp"

/* BSD uses OFILE rather than NOFILE... */
#ifndef RLIMIT_NOFILE
#  define RLIMIT_NOFILE RLIMIT_OFILE
#endif

#define MAX_LINE 2048

static int cfg_reload;    /* 0 on first call, 1 on subsequent calls */
extern int gflags_debug;

static void get_history(std::unique_ptr<cronentry_t> &item);

struct ParseCfgState
{
    ParseCfgState(const std::string &ef, std::vector<StackItem> &stk,
                  std::vector<StackItem> &dstk) :
        stack(stk), deadstack(dstk), ce(nullptr), execfile(ef),
        jobid_st(nullptr), time_st(nullptr), intv_st(nullptr),
        intv2_st(nullptr), strv_st(nullptr), v_strlen(0), linenum(0), v_int(0),
        v_int2(0), cs(0), cmdret(0), intv2_exist(false), runat(false)
    {
        memset(v_str, 0, sizeof v_str);
    }
    char v_str[1024];

    std::vector<StackItem> &stack;
    std::vector<StackItem> &deadstack;
    std::unique_ptr<cronentry_t> ce;

    const std::string execfile;

    const char *jobid_st;
    const char *time_st;
    const char *intv_st;
    const char *intv2_st;
    const char *strv_st;

    size_t v_strlen;
    size_t linenum;

    unsigned int v_time;

    int v_int;
    int v_int2;

    int cs;
    int cmdret;

    bool intv2_exist;
    bool runat;

    void create_ce()
    {
        assert(!ce);
        ce = std::make_unique<cronentry_t>();
        cmdret = 0;
        runat = false;
    }

    inline void debug_print_ce() const
    {
        if (!gflags_debug)
            return;
        fmt::print(stderr, "-=- finish_ce -=-\n");
        fmt::print(stderr, "id: {}\n", ce->id);
        fmt::print(stderr, "command: {}\n", ce->command);
        fmt::print(stderr, "args: {}\n", ce->args);
        fmt::print(stderr, "chroot: {}\n", ce->chroot);
        fmt::print(stderr, "numruns: {}\n", ce->numruns);
        fmt::print(stderr, "maxruns: {}\n", ce->maxruns);
        fmt::print(stderr, "journal: {}\n", ce->journal);
        fmt::print(stderr, "user: {}\n", ce->user);
        fmt::print(stderr, "group: {}\n", ce->group);
        for (const auto &i: ce->month)
            fmt::print(stderr, "month: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->day)
            fmt::print(stderr, "day: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->weekday)
            fmt::print(stderr, "weekday: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->hour)
            fmt::print(stderr, "hour: [{},{}]\n", i.first, i.second);
        for (const auto &i: ce->minute)
            fmt::print(stderr, "minute: [{},{}]\n", i.first, i.second);
        fmt::print(stderr, "interval: {}\n", ce->interval);
        fmt::print(stderr, "exectime: {}\n", ce->exectime);
        fmt::print(stderr, "lasttime: {}\n", ce->lasttime);
    }

    inline void debug_print_ce_history() const
    {
        if (!gflags_debug)
            return;
        fmt::print(stderr, "[{}]->numruns = {}\n", ce->id, ce->numruns);
        fmt::print(stderr, "[{}]->exectime = {}\n", ce->id, ce->exectime);
        fmt::print(stderr, "[{}]->lasttime = {}\n", ce->id, ce->lasttime);
    }

    void finish_ce()
    {
        if (!ce)
            return;
        debug_print_ce();

        if (ce->id <= 0
            || (ce->interval <= 0 && ce->exectime <= 0)
            || ce->command.empty() || cmdret < 1) {
            if (gflags_debug)
                fmt::print(stderr, "===> IGNORE\n");
            ce.reset();
            return;
        }
        if (gflags_debug)
            fmt::print(stderr, "===> ADD\n");

        /* we have a job to insert */
        if (runat) { /* runat task */
            auto forced_exectime = ce->exectime;
            get_history(ce);
            ce->exectime = forced_exectime;
            debug_print_ce_history();

            /* insert iif we haven't exceeded maxruns */
            if (!ce->numruns)
                stack.emplace_back(std::move(ce));
            else
                deadstack.emplace_back(std::move(ce));
        } else { /* interval task */
            get_history(ce);
            debug_print_ce_history();
            set_initial_exectime(*ce);

            /* insert iif numruns < maxruns and no constr error */
            if ((ce->maxruns == 0 || ce->numruns < ce->maxruns)
                && ce->exectime != 0)
                stack.emplace_back(std::move(ce));
            else
                deadstack.emplace_back(std::move(ce));
        }
        ce.reset();
    }

    void setgroupv()
    {
        if (nk_gidbyname(v_str, &ce->group)) {
            fmt::print(stderr, "{}: nonexistent group specified at line {}\n",
                       __func__, linenum);
            std::exit(EXIT_FAILURE);
        }
    }

    void setuserv()
    {
        if (nk_uidgidbyname(v_str, &ce->user, &ce->group)) {
            fmt::print(stderr, "{}: nonexistent user specified at line {}\n",
                       __func__, linenum);
            std::exit(EXIT_FAILURE);
        }
    }

    void setlim(int type)
    {
        struct rlimit rli;
        rli.rlim_cur = v_int == 0 ? RLIM_INFINITY : v_int;
        rli.rlim_max = v_int2 == 0 ? RLIM_INFINITY : v_int2;

        if (!ce->limits)
            ce->limits = std::make_unique<rlimits>();

        switch (type) {
        case RLIMIT_CPU: ce->limits->cpu = rli; break;
        case RLIMIT_FSIZE: ce->limits->fsize = rli; break;
        case RLIMIT_DATA: ce->limits->data = rli; break;
        case RLIMIT_STACK: ce->limits->stack = rli; break;
        case RLIMIT_CORE: ce->limits->core = rli; break;
        case RLIMIT_RSS: ce->limits->rss = rli; break;
        case RLIMIT_NPROC: ce->limits->nproc = rli; break;
        case RLIMIT_NOFILE: ce->limits->nofile = rli; break;
        case RLIMIT_MEMLOCK: ce->limits->memlock = rli; break;
    #ifndef BSD
        case RLIMIT_AS: ce->limits->as = rli; break;
        case RLIMIT_MSGQUEUE: ce->limits->msgqueue = rli; break;
        case RLIMIT_NICE: ce->limits->nice = rli; break;
        case RLIMIT_RTTIME: ce->limits->rttime = rli; break;
        case RLIMIT_RTPRIO: ce->limits->rtprio = rli; break;
        case RLIMIT_SIGPENDING: ce->limits->sigpending = rli; break;
    #endif /* BSD */
        default: fmt::print(stderr, "{}: Bad RLIMIT_type specified.\n", __func__);
                 std::exit(EXIT_FAILURE);
        }
    }

};

struct item_history {
    item_history() {}
    item_history(boost::optional<time_t> e, boost::optional<time_t> l,
                 boost::optional<unsigned int> n) :
        exectime(e), lasttime(l), numruns(n) {}
    boost::optional<time_t> exectime;
    boost::optional<time_t> lasttime;
    boost::optional<unsigned int> numruns;
};

struct hstm {
    hstm() : st(nullptr), cs(0), id(0) {}
    const char *st;
    int cs;
    unsigned int id;
    item_history h;
};

%%{
    machine history_m;
    access hst.;

    action St { hst.st = p; }
    action LastTimeEn { hst.h.lasttime = atoi(hst.st); }
    action NumRunsEn { hst.h.numruns = atoi(hst.st); }
    action ExecTimeEn { hst.h.exectime = atoi(hst.st); }
    action IdEn { hst.id = atoi(hst.st); }

    lasttime = '|' digit+ > St % LastTimeEn;
    numruns = ':' digit+ > St % NumRunsEn;
    exectime = '=' digit+ > St % ExecTimeEn;
    id = digit+ > St % IdEn;
    main := id (numruns | exectime | lasttime)+;
}%%

%% write data;

static int do_parse_history(hstm &hst, const char *p, size_t plen)
{
    const char *pe = p + plen;
    const char *eof = pe;

    %% write init;
    %% write exec;

    if (hst.cs >= history_m_first_final)
        return 1;
    if (hst.cs == history_m_error)
        return -1;
    return -2;
}

static std::unordered_map<unsigned int, item_history> history_map;

static void parse_history(const std::string &path)
{
    char buf[MAX_LINE];
    auto f = fopen(path.c_str(), "r");
    if (!f) {
        fmt::print(stderr, "{}: failed to open history file \"{}\" for read: {}\n",
                   __func__, path, strerror(errno));
        return;
    }
    size_t linenum = 0;
    while (!feof(f)) {
        auto fsv = fgets(buf, sizeof buf, f);
        auto llen = strlen(buf);
        if (buf[llen-1] == '\n')
            buf[--llen] = 0;
        ++linenum;
        if (!fsv) {
            if (!feof(f))
                fmt::print(stderr, "{}: io error fetching line of '{}'\n", __func__, path);
            break;
        }
        if (llen == 0)
            continue;
        hstm h;
        auto r = do_parse_history(h, buf, llen);
        if (r < 0) {
            if (r == -2)
                fmt::print(stderr, "{}: Incomplete configuration at line {}; ignoring\n",
                           __func__, linenum);
            else
                fmt::print(stderr, "{}: Malformed configuration at line {}; ignoring.\n",
                           __func__, linenum);
            continue;
        }
        history_map.emplace(std::make_pair(
            h.id, item_history(h.h.exectime, h.h.lasttime, h.h.numruns)));
    }
    fclose(f);
}

static void get_history(std::unique_ptr<cronentry_t> &item)
{
    assert(item);

    auto i = history_map.find(item->id);
    if (i == history_map.end())
        return;
    if (i->second.exectime) {
        auto exectm = *i->second.exectime;
        item->exectime = exectm > 0 ? exectm : 0;
    }
    if (i->second.lasttime) {
        auto lasttm = *i->second.lasttime;
        item->lasttime = lasttm > 0 ? lasttm : 0;
    }
    if (i->second.numruns) {
        item->numruns = *i->second.numruns;
    }
}

static void addcstlist(ParseCfgState &ncs, cronentry_t::cst_list &list,
                       int wildcard, int min, int max)
{
    int low = ncs.v_int;
    int high = wildcard;
    if (ncs.intv2_exist)
        high = ncs.v_int2;

    if (low > max || low < min)
        low = wildcard;
    if (high > max || high < min)
        high = wildcard;

    /* we don't allow meaningless 'rules' */
    if (low == wildcard && high == wildcard)
        return;

    if (low > high) {
        /* discontinuous range, split into two continuous rules... */
        list.emplace_back(std::make_pair(low, max));
        list.emplace_back(std::make_pair(min, high));
    } else {
        /* handle continuous ranges normally */
        list.emplace_back(std::make_pair(low, high));
    }
}

struct pckm {
    pckm() : st(nullptr), cs(0) {}
    char *st;
    int cs;
};

%%{
    machine parse_cmd_key_m;
    access pckm.;

    action St { pckm.st = p; }
    action CmdEn {
        ncs.ce->command = std::string(pckm.st, p - pckm.st);
        boost::algorithm::replace_all(ncs.ce->command, "\\ ", " ");
        boost::algorithm::replace_all(ncs.ce->command, "\\\\", "\\");
    }
    action ArgEn { ncs.ce->args = std::string(pckm.st, p - pckm.st); }

    sptab = [ \t];
    cmdstr = ([^\0 \t] | '\\\\' | '\\ ')+;
    cmd = sptab* (cmdstr > St % CmdEn);
    args = sptab+ ([^\0])* > St % ArgEn;
    main := cmd args?;
}%%

%% write data;

// cmdret = 0: Not parsed a command key yet.
// cmdret = 1: Success.  Got a command key.
// cmdret = -1: Error: malformed command key.
// cmdret = -2: Error: incomplete command key.
// cmdret = -3: Error: duplicate command key.
static void parse_command_key(ParseCfgState &ncs)
{
    char *p = ncs.v_str;
    const char *pe = ncs.v_str + ncs.v_strlen;
    const char *eof = pe;

    struct pckm pckm;

    if (ncs.cmdret != 0) {
        ncs.cmdret = -3;
        fmt::print(stderr, "Duplicate 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    }

    %% write init;
    %% write exec;

    if (pckm.cs == parse_cmd_key_m_error) {
        ncs.cmdret = -1;
        fmt::print(stderr, "Malformed 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    } else if (pckm.cs >= parse_cmd_key_m_first_final)
        ncs.cmdret = 1;
    else {
        ncs.cmdret = -2;
        fmt::print(stderr, "Incomplete 'command' value at line {}\n",
                   ncs.linenum);
        std::exit(EXIT_FAILURE);
    }
}

%%{
    machine ncrontab;
    access ncs.;

    spc = [ \t];
    eqsep = spc* '=' spc*;

    action TUnitSt { ncs.time_st = p; ncs.v_time = 0; }
    action TSecEn  { ncs.v_time +=          atoi(ncs.time_st); }
    action TMinEn  { ncs.v_time += 60     * atoi(ncs.time_st); }
    action THrEn   { ncs.v_time += 3600   * atoi(ncs.time_st); }
    action TDayEn  { ncs.v_time += 86400  * atoi(ncs.time_st); }
    action TWeekEn { ncs.v_time += 604800 * atoi(ncs.time_st); }

    action IntValSt {
        ncs.intv_st = p;
        ncs.v_int = ncs.v_int2 = 0;
        ncs.intv2_exist = false;
    }
    action IntValEn { ncs.v_int = atoi(ncs.intv_st); }
    action IntVal2St { ncs.intv2_st = p; }
    action IntVal2En {
        ncs.v_int2 = atoi(ncs.intv2_st);
        ncs.intv2_exist = true;
    }

    action StrValSt { ncs.strv_st = p; ncs.v_strlen = 0; }
    action StrValEn {
        ncs.v_strlen = p - ncs.strv_st;
        if (ncs.v_strlen <= INT_MAX) {
            ssize_t snl = snprintf(ncs.v_str, sizeof ncs.v_str,
                                   "%.*s", (int)ncs.v_strlen, ncs.strv_st);
            if (snl < 0 || (size_t)snl >= sizeof ncs.v_str) {
                fmt::print(stderr, "error parsing line {} in crontab; too long?\n",
                           ncs.linenum);
                std::exit(EXIT_FAILURE);
            }
        }
    }

    t_sec  = (digit+ > TUnitSt) 's' % TSecEn;
    t_min  = (digit+ > TUnitSt) 'm' % TMinEn;
    t_hr   = (digit+ > TUnitSt) 'h' % THrEn;
    t_day  = (digit+ > TUnitSt) 'd' % TDayEn;
    t_week = (digit+ > TUnitSt) 'w' % TWeekEn;
    t_any = (t_sec | t_min | t_hr | t_day | t_week);

    intval = (digit+ > IntValSt % IntValEn);
    timeval = t_any (spc* t_any)*;
    intrangeval = (digit+ > IntValSt % IntValEn)
                  (',' (digit+ > IntVal2St % IntVal2En))?;
    stringval = ([^\0\n]+ > StrValSt % StrValEn);

    action JournalEn { ncs.ce->journal = true; }
    journal = 'journal'i % JournalEn;

    action RunAtEn {
        ncs.runat = true;
        ncs.ce->exectime = ncs.v_int;
        ncs.ce->maxruns = 1;
        ncs.ce->journal = true;
    }
    action MaxRunsEn {
        if (!ncs.runat)
            ncs.ce->maxruns = ncs.v_int;
    }

    runat = 'runat'i eqsep intval % RunAtEn;
    maxruns = 'maxruns'i eqsep intval % MaxRunsEn;

    action LimAsEn { ncs.setlim(RLIMIT_AS); }
    action LimMemlockEn { ncs.setlim(RLIMIT_MEMLOCK); }
    action LimNofileEn { ncs.setlim(RLIMIT_NOFILE); }
    action LimNprocEn { ncs.setlim(RLIMIT_NPROC); }
    action LimRssEn { ncs.setlim(RLIMIT_RSS); }
    action LimCoreEn { ncs.setlim(RLIMIT_CORE); }
    action LimStackEn { ncs.setlim(RLIMIT_STACK); }
    action LimDataEn { ncs.setlim(RLIMIT_DATA); }
    action LimFsizeEn { ncs.setlim(RLIMIT_FSIZE); }
    action LimCpuEn { ncs.setlim(RLIMIT_CPU); }
    action LimMsgQueueEn { ncs.setlim(RLIMIT_MSGQUEUE); }
    action LimNiceEn { ncs.setlim(RLIMIT_NICE); }
    action LimRtTimeEn { ncs.setlim(RLIMIT_RTTIME); }
    action LimRtPrioEn { ncs.setlim(RLIMIT_RTPRIO); }
    action LimSigPendingEn { ncs.setlim(RLIMIT_SIGPENDING); }

    lim_as = 'l_as'i eqsep intrangeval % LimAsEn;
    lim_memlock = 'l_memlock'i eqsep intrangeval % LimMemlockEn;
    lim_nofile = 'l_nofile'i eqsep intrangeval % LimNofileEn;
    lim_nproc = 'l_nproc'i eqsep intrangeval % LimNprocEn;
    lim_rss = 'l_rss'i eqsep intrangeval % LimRssEn;
    lim_core = 'l_core'i eqsep intrangeval % LimCoreEn;
    lim_stack = 'l_stack'i eqsep intrangeval % LimStackEn;
    lim_data = 'l_data'i eqsep intrangeval % LimDataEn;
    lim_fsize = 'l_fsize'i eqsep intrangeval % LimFsizeEn;
    lim_cpu = 'l_cpu'i eqsep intrangeval % LimCpuEn;
    lim_msgqueue = 'l_msgqueue'i eqsep intrangeval % LimMsgQueueEn;
    lim_nice = 'l_nice'i eqsep intrangeval % LimNiceEn;
    lim_rttime = 'l_rttime'i eqsep intrangeval % LimRtTimeEn;
    lim_rtprio = 'l_rtprio'i eqsep intrangeval % LimRtPrioEn;
    lim_sigpending = 'l_sigpending'i eqsep intrangeval % LimSigPendingEn;

    action IntervalEn { ncs.ce->interval = ncs.v_time; }

    interval = 'interval'i eqsep timeval % IntervalEn;

    action MonthEn { addcstlist(ncs, ncs.ce->month, 0, 1, 12); }
    action DayEn { addcstlist(ncs, ncs.ce->day, 0, 1, 31); }
    action WeekdayEn { addcstlist(ncs, ncs.ce->weekday, 0, 1, 7); }
    action HourEn { addcstlist(ncs, ncs.ce->hour, 24, 0, 23); }
    action MinuteEn { addcstlist(ncs, ncs.ce->minute, 60, 0, 59); }

    month = 'month'i eqsep intrangeval % MonthEn;
    day = 'day'i eqsep intrangeval % DayEn;
    weekday = 'weekday'i eqsep intrangeval % WeekdayEn;
    hour = 'hour'i eqsep intrangeval % HourEn;
    minute = 'minute'i eqsep intrangeval % MinuteEn;

    action GroupEn { ncs.setgroupv(); }
    action UserEn { ncs.setuserv(); }
    action ChrootEn {
        ncs.ce->chroot = std::string(ncs.v_str, ncs.v_strlen);
    }
    action CommandEn { parse_command_key(ncs); }

    group = 'group'i eqsep stringval % GroupEn;
    user = 'user'i eqsep stringval % UserEn;
    chroot = 'chroot'i eqsep stringval % ChrootEn;
    command = 'command'i eqsep stringval % CommandEn;

    cmds = command | chroot | user | group | minute | hour | weekday | day |
           month | interval | lim_cpu | lim_fsize | lim_data | lim_stack |
           lim_core | lim_rss | lim_nproc | lim_nofile | lim_memlock | lim_as |
           lim_msgqueue | lim_nice | lim_rttime | lim_rtprio | lim_sigpending |
           maxruns | runat | journal;

    action JobIdSt { ncs.jobid_st = p; }
    action JobIdEn { ncs.ce->id = atoi(ncs.jobid_st); }
    action CreateCe { ncs.finish_ce(); ncs.create_ce(); }

    jobid = ('!' > CreateCe) (digit+ > JobIdSt) % JobIdEn;

    main := jobid | cmds;
}%%

%% write data;

static int do_parse_config(ParseCfgState &ncs, const char *p, size_t plen)
{
    const char *pe = p + plen;
    const char *eof = pe;

    %% write init;
    %% write exec;

    if (ncs.cs == ncrontab_error)
        return -1;
    if (ncs.cs >= ncrontab_first_final)
        return 1;
    return 0;
}

void parse_config(const std::string &path, const std::string &execfile,
                  std::vector<StackItem> &stk,
                  std::vector<StackItem> &deadstk)
{
    struct ParseCfgState ncs(execfile, stk, deadstk);
    parse_history(ncs.execfile);

    char buf[MAX_LINE];
    auto f = fopen(path.c_str(), "r");
    if (!f) {
        fmt::print(stderr, "{}: failed to open file: '{}': {}\n",
                   __func__, path, strerror(errno));
        std::exit(EXIT_FAILURE);
    }
    while (!feof(f)) {
        auto fsv = fgets(buf, sizeof buf, f);
        auto llen = strlen(buf);
        if (buf[llen-1] == '\n')
            buf[--llen] = 0;
        ++ncs.linenum;
        if (!fsv) {
            if (!feof(f))
                fmt::print(stderr, "{}: io error fetching line of '{}'\n", __func__, path);
            break;
        }
        if (llen == 0)
            continue;
        auto r = do_parse_config(ncs, buf, llen);
        if (r < 0) {
            fmt::print(stderr, "{}: do_parse_config({}) failed at line {}\n",
                       __func__, path, ncs.linenum);
            std::exit(EXIT_FAILURE);
        }
    }
    std::make_heap(stk.begin(), stk.end(), GtCronEntry);
    history_map.clear();
    cfg_reload = 1;
    fclose(f);
}


use static approach - better performance

%%machine lexer;

module CakeLang
  class Lexer
    %% write data;

    CONVERSION = {
        :T_INT => :to_i,
        :T_FLOAT => :to_f
      }

    def initialize
      reset
    end

    def reset
      @data     = nil
      @ts       = nil
      @te       = nil
      @tokens   = []
    end

    # @ts, @te -> token start, token end
    # had to use @te-1 for token range end because 1 digit token (eg.: ',(,),+) and no whitespace between will
    # break the tokenization
    def data(start = @ts, stop = (@te-1))
      return @data[start..stop].pack('U*')
    end

    def emit(symbol, start = @ts, stop = (@te-1))
      value = data(start, stop)

      if symbol.eql?(:T_INT) or symbol.eql?(:T_FLOAT)
        value = value.send(CONVERSION[symbol])
      end

      @tokens << [symbol, value]
    end

    def lex(data)
      @data = data.unpack("U*")
      lexer_start = self.class.lexer_start
      eof = data.length

      _lexer_eof_trans          = self.class.send(:_lexer_eof_trans)
      _lexer_from_state_actions = self.class.send(:_lexer_from_state_actions)
      _lexer_index_offsets      = self.class.send(:_lexer_index_offsets)
      _lexer_indicies           = self.class.send(:_lexer_indicies)
      _lexer_key_spans          = self.class.send(:_lexer_key_spans)
      _lexer_to_state_actions   = self.class.send(:_lexer_to_state_actions)
      _lexer_trans_actions      = self.class.send(:_lexer_trans_actions)
      _lexer_trans_keys         = self.class.send(:_lexer_trans_keys)
      _lexer_trans_targs        = self.class.send(:_lexer_trans_targs)

      %% write init;
      %% write exec;

      tokens = @tokens

      reset

      return tokens
    end

    %%{
      access @;

      T_KEYWORD_DEF = '->';
      T_KEYWORD_END = '<-';
      T_KEYWORD_OUT = 'puts';
      T_DQUOTE      = '"';
      T_INT         = ('+'|'-')?[0-9]+;
      T_FLOAT       = ('+'|'-')?[0-9]+'.'[0-9];
      T_LITERAL     = [a-zA-Z_][a-zA-Z_0-9]*;
      T_ADD         = '+';
      T_SUB         = '-';
      T_MUL         = '*';
      T_DIV         = '/';
      T_MOD         = '%';
      T_EXP         = '^';
      T_LBR         = '(';
      T_RBR         = ')';
      T_EQ          = '=';
      T_COL         = ',';

      main := |*

        T_KEYWORD_DEF => {
          emit(:T_KEYWORD_DEF)
        };

        T_KEYWORD_END => {
          emit(:T_KEYWORD_END)
        };

        T_KEYWORD_OUT => {
          emit(:T_KEYWORD_OUT)
        };

        T_DQUOTE => {
          emit(:T_DQUOTE)
        };

        T_INT => {
          emit(:T_INT)
        };

        T_FLOAT => {
          emit(:T_FLOAT)
        };

        T_LITERAL => {
          emit(:T_LITERAL)
        };

        T_ADD => {
          emit(:T_ADD)
        };

        T_SUB => {
          emit(:T_SUB)
        };

        T_MUL => {
          emit(:T_MUL)
        };

        T_DIV => {
          emit(:T_DIV)
        };

        T_EXP => {
          emit(:T_EXP)
        };

        T_MOD => {
          emit(:T_MOD)
        };

        T_LBR => {
          emit(:T_LBR)
        };

        T_RBR => {
          emit(:T_RBR)
        };

        T_EQ => {
          emit(:T_EQ)
        };

        T_COL => {
          emit(:T_COL)
        };

        space;

        # any+ => {
          # emit(:UNKNOWN)
        # };

      *|;

    }%%
  end # Lexer
end # CakeLang

%%machine lexer;

module CakeLang
  class Lexer
    %% write data;

    CONVERSION = {
        :T_INT => :to_i,
        :T_FLOAT => :to_f
      }

    # Lazy way of forwarding instance method calls used internally by Ragel to
    # their corresponding class methods.
    private_methods.grep(/^_lexer_/).each do |name|
      define_method(name) do
        return self.class.send(name)
      end

      private(name)
    end

    def initialize
      reset
    end

    def reset
      @data     = nil
      @ts       = nil
      @te       = nil
      @tokens   = []
    end

    # @ts, @te -> token start, token end
    # had to use @te-1 for token range end because 1 digit token (eg.: ',(,),+) and no whitespace between will
    # break the tokenization
    def data(start = @ts, stop = (@te-1))
      return @data[start..stop].pack('U*')
    end

    def emit(symbol, start = @ts, stop = (@te-1))
      value = data(start, stop)

      if symbol.eql?(:T_INT) or symbol.eql?(:T_FLOAT)
        value = value.send(CONVERSION[symbol])
      end

      @tokens << [symbol, value]
    end

    def lex(data)
      @data = data.unpack("U*")
      lexer_start = self.class.lexer_start
      eof = data.length

      %% write init;
      %% write exec;

      tokens = @tokens

      reset

      return tokens
    end

    %%{
      access @;

      T_KEYWORD_DEF = '->';
      T_KEYWORD_END = '<-';
      T_KEYWORD_OUT = 'puts';
      T_DQUOTE      = '"';
      T_INT         = ('+'|'-')?[0-9]+;
      T_FLOAT       = ('+'|'-')?[0-9]+'.'[0-9];
      T_LITERAL     = [a-zA-Z_][a-zA-Z_0-9]*;
      T_ADD         = '+';
      T_SUB         = '-';
      T_MUL         = '*';
      T_DIV         = '/';
      T_MOD         = '%';
      T_EXP         = '^';
      T_LBR         = '(';
      T_RBR         = ')';
      T_EQ          = '=';
      T_COL         = ',';

      main := |*

        T_KEYWORD_DEF => {
          emit(:T_KEYWORD_DEF)
        };

        T_KEYWORD_END => {
          emit(:T_KEYWORD_END)
        };

        T_KEYWORD_OUT => {
          emit(:T_KEYWORD_OUT)
        };

        T_DQUOTE => {
          emit(:T_DQUOTE)
        };

        T_INT => {
          emit(:T_INT)
        };

        T_FLOAT => {
          emit(:T_FLOAT)
        };

        T_LITERAL => {
          emit(:T_LITERAL)
        };

        T_ADD => {
          emit(:T_ADD)
        };

        T_SUB => {
          emit(:T_SUB)
        };

        T_MUL => {
          emit(:T_MUL)
        };

        T_DIV => {
          emit(:T_DIV)
        };

        T_EXP => {
          emit(:T_EXP)
        };

        T_MOD => {
          emit(:T_MOD)
        };

        T_LBR => {
          emit(:T_LBR)
        };

        T_RBR => {
          emit(:T_RBR)
        };

        T_EQ => {
          emit(:T_EQ)
        };

        T_COL => {
          emit(:T_COL)
        };

        space;

        # any+ => {
          # emit(:UNKNOWN)
        # };

      *|;

    }%%
  end # Lexer
end # CakeLang


formatting

formatting

#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

#include "qstruct/compiler.h"

int calculate_qstruct_packing(struct qstruct_definition *);

%%{
  machine qstruct;
  write data;
}%%

struct qstruct_definition *parse_qstructs(char *schema, size_t schema_size, char *err_buf, size_t err_buf_size) {
  char *p = schema, *pe = schema + schema_size, *eof = 0;
  int cs = -1;

  int curr_line = 1;
  ssize_t i;
  size_t j;
  int err = 0;

  struct qstruct_definition *def = NULL, *new_def, *curr_def, *temp_def;
  struct qstruct_item curr_item;
  ssize_t curr_item_index;
  struct qstruct_definition *def_hash_by_name = NULL, *def_lookup;

  struct qstruct_item *new_items;
  ssize_t items_allocated;
  ssize_t largest_item;
  struct qstruct_item *item_hash_by_name = NULL, *item_lookup;

  char err_ctx_buf[256];
  char err_desc_buf[512];
  char *err_ctx_start, *err_ctx_end;

  // Parsing phase

  // the ragel state machine will initialise these variables, assignments silence compiler warnings
  curr_item_index = 0;
  memset(&curr_item, '\0', sizeof(curr_item));
  items_allocated = 0;
  largest_item = -1;

  #define PARSE_ERROR(...) do { \
    snprintf(err_desc_buf, sizeof(err_desc_buf), __VA_ARGS__); \
    for(err_ctx_start=p; err_ctx_start>schema && *err_ctx_start != '\n' && (p-err_ctx_start) < 20; err_ctx_start--) {} \
    while (isspace(*err_ctx_start) && err_ctx_start < p) err_ctx_start++; \
    for(err_ctx_end=p; err_ctx_end<(pe-1) && *err_ctx_end != '\n' && (err_ctx_end-p) < 20; err_ctx_end++) {} \
    memcpy(err_ctx_buf, err_ctx_start, err_ctx_end - err_ctx_start); \
    *(err_ctx_buf + (err_ctx_end - err_ctx_start)) = '\0'; \
    snprintf(err_buf, err_buf_size, "\n------------------------------------------------------------\nQstruct schema parse error (line %d, character %d)\n\n  %s\n  %*s^\n  %*s|--%s\n\n------------------------------------------------------------\n", curr_line, (int)(p-schema), err_ctx_buf, (int)(p-err_ctx_start), " ", (int)(p-err_ctx_start), " ", err_desc_buf); \
    err = 1; \
    goto bail; \
  } while(0)

  %%{
    action init_qstruct {
      new_def = malloc(sizeof(struct qstruct_definition));
      if (new_def == NULL)
        PARSE_ERROR("out of memory");

      new_def->next = def;
      def = new_def;
      new_def = NULL;

      items_allocated = 64;
      def->items = malloc(items_allocated * sizeof(struct qstruct_item));
      if (def->items == NULL)
        PARSE_ERROR("out of memory");

      largest_item = -1;
      for (i=0; i<items_allocated; i++) def->items[i].occupied = 0;
    }

    action handle_item {
      if (curr_item_index < 0)
        PARSE_ERROR("id value overflow"); // FIXME: prolly need to check upper bound

      if (curr_item_index >= items_allocated) {
        new_items = realloc(def->items, curr_item_index*2 * sizeof(struct qstruct_item));
        if (new_items == NULL)
          PARSE_ERROR("out of memory");

        def->items = new_items;
        new_items = NULL;

        for(i=items_allocated; i<curr_item_index*2; i++) def->items[i].occupied = 0;
        items_allocated = curr_item_index*2;
      }

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_BOOL && (curr_item.type & (QSTRUCT_TYPE_MOD_ARRAY_FIX | QSTRUCT_TYPE_MOD_ARRAY_DYN)))
        PARSE_ERROR("bools can't be arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_STRING && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("strings can't be fixed-size arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_BLOB && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("blobs can't be fixed-size arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_NESTED && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("nested qstructs can't be fixed-size arrays");

      if (def->items[curr_item_index].occupied)
        PARSE_ERROR("duplicated index %ld", curr_item_index);

      def->items[curr_item_index].name = curr_item.name;
      def->items[curr_item_index].name_len = curr_item.name_len;
      def->items[curr_item_index].type = curr_item.type;
      def->items[curr_item_index].fixed_array_size = curr_item.fixed_array_size;
      def->items[curr_item_index].nested_name = curr_item.nested_name;
      def->items[curr_item_index].nested_name_len = curr_item.nested_name_len;
      def->items[curr_item_index].occupied = 1;

      if (curr_item_index > largest_item) largest_item = curr_item_index;
    }

    action handle_qstruct {
      assert(!item_hash_by_name);

      for(i=0; i<=largest_item; i++) {
        if (!def->items[i].occupied)
          PARSE_ERROR("missing item %ld", i);

        HASH_FIND(hh, item_hash_by_name, def->items[i].name, def->items[i].name_len, item_lookup);
        if (item_lookup)
          PARSE_ERROR("duplicate item name '%.*s'", (int) def->items[i].name_len, def->items[i].name);

        HASH_ADD_KEYPTR(hh, item_hash_by_name, def->items[i].name, def->items[i].name_len, &def->items[i]);
      }

      HASH_CLEAR(hh, item_hash_by_name);

      def[0].num_items = largest_item+1;
    }

    newline = '\n' @{ curr_line++; };
    any_count_line = any | newline;
    whitespace_char = any_count_line - 0x21..0x7e;

    alnum_u = alnum | '_';
    lc_alpha_u = [a-z] | '_';
    uc_alpha_u = [A-Z] | '_';
    identifier = lc_alpha_u $!{ PARSE_ERROR("qstruct item names must start with lowercase letters"); }
                 alnum_u*;
    identifier_with_package = uc_alpha_u $!{ PARSE_ERROR("qstruct names must start with uppercase letters"); }
                              alnum_u* ('::' uc_alpha_u alnum_u*)*;
    integer = digit+;

    ws = whitespace_char |
         '#' [^\n]* newline |
         '/*' (any_count_line* - (any_count_line* '*/' any_count_line*)) '*/'
      ;

    type = 'string' %{ curr_item.type = QSTRUCT_TYPE_STRING; } |
           'blob' %{ curr_item.type = QSTRUCT_TYPE_BLOB; } |
           'bool' %{ curr_item.type = QSTRUCT_TYPE_BOOL; } |
           'float' %{ curr_item.type = QSTRUCT_TYPE_FLOAT; } |
           'double' %{ curr_item.type = QSTRUCT_TYPE_DOUBLE; } |
           'int8' %{ curr_item.type = QSTRUCT_TYPE_INT8; } |
           'uint8' %{ curr_item.type = QSTRUCT_TYPE_INT8 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int16' %{ curr_item.type = QSTRUCT_TYPE_INT16; } |
           'uint16' %{ curr_item.type = QSTRUCT_TYPE_INT16 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int32' %{ curr_item.type = QSTRUCT_TYPE_INT32; } |
           'uint32' %{ curr_item.type = QSTRUCT_TYPE_INT32 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int64' %{ curr_item.type = QSTRUCT_TYPE_INT64; } |
           'uint64' %{ curr_item.type = QSTRUCT_TYPE_INT64 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           identifier_with_package >{ curr_item.nested_name = p; curr_item.type = QSTRUCT_TYPE_NESTED; }
                                   %{ curr_item.nested_name_len = p - curr_item.nested_name; }
      ;

    array_spec = '['
                   ws*
                   integer >{ curr_item.fixed_array_size = 0; }
                           @{ curr_item.fixed_array_size = curr_item.fixed_array_size * 10 + (fc - '0'); }
                   ws*
                 ']' >{ curr_item.type |= QSTRUCT_TYPE_MOD_ARRAY_FIX; }

                  |

                 '[' ws* ']' >{ curr_item.type |= QSTRUCT_TYPE_MOD_ARRAY_DYN; }
      ;

    item = identifier >{ curr_item.name = p; curr_item.fixed_array_size = 1; }
                      %{ curr_item.name_len = p - curr_item.name; }
                      $!{ PARSE_ERROR("invalid identifier"); }
           ws+
           '@' $!{ PARSE_ERROR("expected @ id"); }
           integer >{ curr_item_index = 0; }
                   @{ curr_item_index = curr_item_index * 10 + (fc - '0'); }
           ws+
           type $!{ PARSE_ERROR("unrecognized type"); }
           ws*
           array_spec? $!{ PARSE_ERROR("invalid array specifier"); }
           ws* ';' $!{ PARSE_ERROR("missing semi-colon"); }
      ;

    qstruct = ws*
              ( [qQ] 'struct' ) >init_qstruct
                                $!{ PARSE_ERROR("expected qstruct definition"); }
              ws+
              identifier_with_package >{ def[0].name = p; }
                                      %{ def[0].name_len = p - def[0].name; }
              ws*
             '{'
               ws* (item @handle_item ws*)*
             '}' @handle_qstruct
             (ws* ';')?
      ;

    main := qstruct* ws*;

    write init;
    write exec;
  }%%

  if (cs < qstruct_first_final)
    PARSE_ERROR("general parse error");

  // Compilation phase

  // reverse def list
  for (curr_def = NULL; def;) {
    temp_def = def->next;
    def->next = curr_def;
    curr_def = def;
    def = temp_def;
  }
  def = curr_def;

  assert(!def_hash_by_name);

  for(curr_def = def; curr_def; curr_def = curr_def->next) {
    for(j=0; j<def->num_items; j++) {
      if ((def->items[j].type & 0xFFFF) == QSTRUCT_TYPE_NESTED) {
        HASH_FIND(hh, def_hash_by_name, def->items[j].nested_name, def->items[j].nested_name_len, def_lookup);
        if (!def_lookup)
          PARSE_ERROR("type '%.*s' referred to before being defined", (int) def->items[j].nested_name_len, def->items[j].nested_name);

        def->items[j].nested_def = def_lookup;
      }
    }

    HASH_FIND(hh, def_hash_by_name, curr_def->name, curr_def->name_len, def_lookup);
    if (def_lookup)
      PARSE_ERROR("duplicate def name '%.*s'", (int) curr_def->name_len, curr_def->name);

    HASH_ADD_KEYPTR(hh, def_hash_by_name, curr_def->name, curr_def->name_len, curr_def);

    if (calculate_qstruct_packing(curr_def) < 0)
      PARSE_ERROR("memory error in packing");
  }

  bail:

  HASH_CLEAR(hh, item_hash_by_name);
  HASH_CLEAR(hh, def_hash_by_name);

  if (err) {
    free_qstruct_definitions(def);
    return NULL;
  }

  return def;
}

void free_qstruct_definitions(struct qstruct_definition *def) {
  struct qstruct_definition *temp;

  while (def) {
    if (def->items) free(def->items);
    temp = def->next;
    free(def);
    def = temp;
  }
}

#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <assert.h>

#include "qstruct/compiler.h"

int calculate_qstruct_packing(struct qstruct_definition *);

%%{
  machine qstruct;
  write data;
}%%

struct qstruct_definition *parse_qstructs(char *schema, size_t schema_size, char *err_buf, size_t err_buf_size) {
  char *p = schema, *pe = schema + schema_size, *eof = 0;
  int cs = -1;

  int curr_line = 1;
  ssize_t i;
  size_t j;
  int err = 0;

  struct qstruct_definition *def = NULL, *new_def, *curr_def, *temp_def;
  struct qstruct_item curr_item;
  ssize_t curr_item_index;
  struct qstruct_definition *def_hash_by_name = NULL, *def_lookup;

  struct qstruct_item *new_items;
  ssize_t items_allocated;
  ssize_t largest_item;
  struct qstruct_item *item_hash_by_name = NULL, *item_lookup;

  char err_ctx_buf[256];
  char err_desc_buf[512];
  char *err_ctx_start, *err_ctx_end;

  // Parsing phase

  // the ragel state machine will initialise these variables, assignments silence compiler warnings
  curr_item_index = 0;
  memset(&curr_item, '\0', sizeof(curr_item));
  items_allocated = 0;
  largest_item = -1;

  #define PARSE_ERROR(...) do { \
    snprintf(err_desc_buf, sizeof(err_desc_buf), __VA_ARGS__); \
    for(err_ctx_start=p; err_ctx_start>schema && *err_ctx_start != '\n' && (p-err_ctx_start) < 20; err_ctx_start--) {} \
    while (isspace(*err_ctx_start) && err_ctx_start < p) err_ctx_start++; \
    for(err_ctx_end=p; err_ctx_end<(pe-1) && *err_ctx_end != '\n' && (err_ctx_end-p) < 20; err_ctx_end++) {} \
    memcpy(err_ctx_buf, err_ctx_start, err_ctx_end - err_ctx_start); \
    *(err_ctx_buf + (err_ctx_end - err_ctx_start)) = '\0'; \
    snprintf(err_buf, err_buf_size, "\n------------------------------------------------------------\nQstruct schema parse error (line %d, character %d)\n\n  %s\n  %*s^\n  %*s|--%s\n\n------------------------------------------------------------\n", curr_line, (int)(p-schema), err_ctx_buf, (int)(p-err_ctx_start), " ", (int)(p-err_ctx_start), " ", err_desc_buf); \
    err = 1; \
    goto bail; \
  } while(0)

  %%{
    action init_qstruct {
      new_def = malloc(sizeof(struct qstruct_definition));
      if (new_def == NULL)
        PARSE_ERROR("out of memory");

      new_def->next = def;
      def = new_def;
      new_def = NULL;

      items_allocated = 64;
      def->items = malloc(items_allocated * sizeof(struct qstruct_item));
      if (def->items == NULL)
        PARSE_ERROR("out of memory");

      largest_item = -1;
      for (i=0; i<items_allocated; i++) def->items[i].occupied = 0;
    }

    action handle_item {
      if (curr_item_index < 0)
        PARSE_ERROR("id value overflow"); // FIXME: prolly need to check upper bound

      if (curr_item_index >= items_allocated) {
        new_items = realloc(def->items, curr_item_index*2 * sizeof(struct qstruct_item));
        if (new_items == NULL)
          PARSE_ERROR("out of memory");

        def->items = new_items;
        new_items = NULL;

        for(i=items_allocated; i<curr_item_index*2; i++) def->items[i].occupied = 0;
        items_allocated = curr_item_index*2;
      }

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_BOOL && (curr_item.type & (QSTRUCT_TYPE_MOD_ARRAY_FIX | QSTRUCT_TYPE_MOD_ARRAY_DYN)))
        PARSE_ERROR("bools can't be arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_STRING && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("strings can't be fixed-size arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_BLOB && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("blobs can't be fixed-size arrays");

      if ((curr_item.type & 0xFFFF) == QSTRUCT_TYPE_NESTED && (curr_item.type & QSTRUCT_TYPE_MOD_ARRAY_FIX))
        PARSE_ERROR("nested qstructs can't be fixed-size arrays");

      if (def->items[curr_item_index].occupied)
        PARSE_ERROR("duplicated index %ld", curr_item_index);

      def->items[curr_item_index].name = curr_item.name;
      def->items[curr_item_index].name_len = curr_item.name_len;
      def->items[curr_item_index].type = curr_item.type;
      def->items[curr_item_index].fixed_array_size = curr_item.fixed_array_size;
      def->items[curr_item_index].nested_name = curr_item.nested_name;
      def->items[curr_item_index].nested_name_len = curr_item.nested_name_len;
      def->items[curr_item_index].occupied = 1;

      if (curr_item_index > largest_item) largest_item = curr_item_index;
    }

    action handle_qstruct {
      assert(!item_hash_by_name);

      for(i=0; i<=largest_item; i++) {
        if (!def->items[i].occupied)
          PARSE_ERROR("missing item %ld", i);

        HASH_FIND(hh, item_hash_by_name, def->items[i].name, def->items[i].name_len, item_lookup);
        if (item_lookup)
          PARSE_ERROR("duplicate item name '%.*s'", (int) def->items[i].name_len, def->items[i].name);

        HASH_ADD_KEYPTR(hh, item_hash_by_name, def->items[i].name, def->items[i].name_len, &def->items[i]);
      }

      HASH_CLEAR(hh, item_hash_by_name);

      def[0].num_items = largest_item+1;
    }

    newline = '\n' @{curr_line++;};
    any_count_line = any | newline;
    whitespace_char = any_count_line - 0x21..0x7e;

    alnum_u = alnum | '_';
    lc_alpha_u = [a-z] | '_';
    uc_alpha_u = [A-Z] | '_';
    identifier = lc_alpha_u $!{ PARSE_ERROR("qstruct item names must start with lowercase letters"); }
                 alnum_u*;
    identifier_with_package = uc_alpha_u $!{ PARSE_ERROR("qstruct names must start with uppercase letters"); }
                              alnum_u* ('::' uc_alpha_u alnum_u*)*;
    integer = digit+;

    ws = (
           whitespace_char |
           ( '#' [^\n]* newline ) |
           ( '/*' ( ( any_count_line )* - ( any_count_line* '*/' any_count_line* ) ) '*/' )
         );

    type = 'string' %{ curr_item.type = QSTRUCT_TYPE_STRING; } |
           'blob' %{ curr_item.type = QSTRUCT_TYPE_BLOB; } |
           'bool' %{ curr_item.type = QSTRUCT_TYPE_BOOL; } |
           'float' %{ curr_item.type = QSTRUCT_TYPE_FLOAT; } |
           'double' %{ curr_item.type = QSTRUCT_TYPE_DOUBLE; } |
           'int8' %{ curr_item.type = QSTRUCT_TYPE_INT8; } |
           'uint8' %{ curr_item.type = QSTRUCT_TYPE_INT8 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int16' %{ curr_item.type = QSTRUCT_TYPE_INT16; } |
           'uint16' %{ curr_item.type = QSTRUCT_TYPE_INT16 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int32' %{ curr_item.type = QSTRUCT_TYPE_INT32; } |
           'uint32' %{ curr_item.type = QSTRUCT_TYPE_INT32 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           'int64' %{ curr_item.type = QSTRUCT_TYPE_INT64; } |
           'uint64' %{ curr_item.type = QSTRUCT_TYPE_INT64 | QSTRUCT_TYPE_MOD_UNSIGNED; } |
           identifier_with_package >{ curr_item.nested_name = p; curr_item.type = QSTRUCT_TYPE_NESTED; }
                                   %{ curr_item.nested_name_len = p - curr_item.nested_name; }
      ;

    array_spec = '['
                    ws*
                    integer >{ curr_item.fixed_array_size = 0; }
                            @{ curr_item.fixed_array_size = curr_item.fixed_array_size * 10 + (fc - '0'); }
                    ws*
                  ']' >{ curr_item.type |= QSTRUCT_TYPE_MOD_ARRAY_FIX; }

                   |

                   '[' ws* ']' >{ curr_item.type |= QSTRUCT_TYPE_MOD_ARRAY_DYN; }
                 ;

    item = identifier >{ curr_item.name = p; curr_item.fixed_array_size = 1; }
                      %{ curr_item.name_len = p - curr_item.name; }
                      $!{ PARSE_ERROR("invalid identifier"); }
           ws+
           '@' $!{ PARSE_ERROR("expected @ id"); }
           integer >{ curr_item_index = 0; }
                   @{ curr_item_index = curr_item_index * 10 + (fc - '0'); }
           ws+
           type $!{ PARSE_ERROR("unrecognized type"); }
           ws*
           array_spec?
             $!{ PARSE_ERROR("invalid array specifier"); }
           ws* ';' $!{ PARSE_ERROR("missing semi-colon"); }
      ;

    qstruct = ws*
              ( [qQ] 'struct' ) >init_qstruct
                                $!{ PARSE_ERROR("expected qstruct definition"); }
              ws+
              identifier_with_package >{ def[0].name = p; }
                                      %{ def[0].name_len = p - def[0].name; }
              ws*
             '{'
               ws* (item @handle_item ws*)*
             '}' @handle_qstruct
             (ws* ';')?
      ;

    ########################

    main := qstruct* ws*;

    write init;
    write exec;
  }%%

  if (cs < qstruct_first_final)
    PARSE_ERROR("general parse error");

  // Compilation phase

  // reverse def list
  for (curr_def=NULL; def;) {
    temp_def = def->next;
    def->next = curr_def;
    curr_def = def;
    def = temp_def;
  }
  def = curr_def;

  assert(!def_hash_by_name);

  for(curr_def = def; curr_def; curr_def = curr_def->next) {
    for(j=0; j<def->num_items; j++) {
      if ((def->items[j].type & 0xFFFF) == QSTRUCT_TYPE_NESTED) {
        HASH_FIND(hh, def_hash_by_name, def->items[j].nested_name, def->items[j].nested_name_len, def_lookup);
        if (!def_lookup)
          PARSE_ERROR("type '%.*s' referred to before being defined", (int) def->items[j].nested_name_len, def->items[j].nested_name);

        def->items[j].nested_def = def_lookup;
      }
    }

    HASH_FIND(hh, def_hash_by_name, curr_def->name, curr_def->name_len, def_lookup);
    if (def_lookup)
      PARSE_ERROR("duplicate def name '%.*s'", (int) curr_def->name_len, curr_def->name);

    HASH_ADD_KEYPTR(hh, def_hash_by_name, curr_def->name, curr_def->name_len, curr_def);

    if (calculate_qstruct_packing(curr_def) < 0)
      PARSE_ERROR("memory error in packing");
  }

  bail:

  HASH_CLEAR(hh, item_hash_by_name);
  HASH_CLEAR(hh, def_hash_by_name);

  if (err) {
    free_qstruct_definitions(def);
    return NULL;
  }

  return def;
}

void free_qstruct_definitions(struct qstruct_definition *def) {
  struct qstruct_definition *temp;

  while (def) {
    if (def->items) free(def->items);
    temp = def->next;
    free(def);
    def = temp;
  }
}


Remove line counting, unneeded.

Remove line counting, unneeded.