http: add status to http_response

http: add status to http_response

Our HTTP response parser chooses to ignore the status code
and not keep it anywhere. It's quite useful to be able to
inspect the received status code, so this value is now kept
in the http_response structure.

/*
 * This file is open source software, licensed to you under the terms
 * of the Apache License, Version 2.0 (the "License").  See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership.  You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Copyright (C) 2015 Cloudius Systems, Ltd.
 */

#include <seastar/core/ragel.hh>
#include <memory>
#include <unordered_map>

namespace seastar {

struct http_response {
    sstring _version;
    std::unordered_map<sstring, sstring> _headers;
    int _status_code;
};

%% machine http_response;

%%{

access _fsm_;

action mark {
    g.mark_start(p);
}

action store_version {
    _rsp->_version = str();
}

action store_field_name {
    _field_name = str();
}

action store_value {
    _value = str();
}

action assign_field {
    _rsp->_headers[_field_name] = std::move(_value);
}

action extend_field  {
    _rsp->_headers[_field_name] += sstring(" ") + std::move(_value);
}

action store_status {
    _rsp->_status_code = std::atoi(str().c_str());
}

action done {
    done = true;
    fbreak;
}

cr = '\r';
lf = '\n';
crlf = '\r\n';
tchar = alpha | digit | '-' | '!' | '#' | '$' | '%' | '&' | '\'' | '*'
        | '+' | '.' | '^' | '_' | '`' | '|' | '~';

sp = ' ';
ht = '\t';

sp_ht = sp | ht;

http_version = 'HTTP/' (digit '.' digit) >mark %store_version;

field = tchar+ >mark %store_field_name;
value = any* >mark %store_value;
status_code = (digit digit digit) >mark %store_status;
start_line = http_version space status_code space (any - cr - lf)* crlf;
header_1st = (field sp_ht* ':' value :> crlf) %assign_field;
header_cont = (sp_ht+ value sp_ht* crlf) %extend_field;
header = header_1st header_cont*;
main := start_line header* :> (crlf @done);

}%%

class http_response_parser : public ragel_parser_base<http_response_parser> {
    %% write data nofinal noprefix;
public:
    enum class state {
        error,
        eof,
        done,
    };
    std::unique_ptr<http_response> _rsp;
    sstring _field_name;
    sstring _value;
    state _state;
public:
    void init() {
        init_base();
        _rsp.reset(new http_response());
        _state = state::eof;
        %% write init;
    }
    char* parse(char* p, char* pe, char* eof) {
        sstring_builder::guard g(_builder, p, pe);
        auto str = [this, &g, &p] { g.mark_end(p); return get_str(); };
        bool done = false;
        if (p != pe) {
            _state = state::error;
        }
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmisleading-indentation"
#endif
        %% write exec;
#ifdef __clang__
#pragma clang diagnostic pop
#endif
        if (!done) {
            p = nullptr;
        } else {
            _state = state::done;
        }
        return p;
    }
    auto get_parsed_response() {
        return std::move(_rsp);
    }
    bool eof() const {
        return _state == state::eof;
    }
};

}

/*
 * This file is open source software, licensed to you under the terms
 * of the Apache License, Version 2.0 (the "License").  See the NOTICE file
 * distributed with this work for additional information regarding copyright
 * ownership.  You may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
/*
 * Copyright (C) 2015 Cloudius Systems, Ltd.
 */

#include <seastar/core/ragel.hh>
#include <memory>
#include <unordered_map>

namespace seastar {

struct http_response {
    sstring _version;
    std::unordered_map<sstring, sstring> _headers;
};

%% machine http_response;

%%{

access _fsm_;

action mark {
    g.mark_start(p);
}

action store_version {
    _rsp->_version = str();
}

action store_field_name {
    _field_name = str();
}

action store_value {
    _value = str();
}

action assign_field {
    _rsp->_headers[_field_name] = std::move(_value);
}

action extend_field  {
    _rsp->_headers[_field_name] += sstring(" ") + std::move(_value);
}

action done {
    done = true;
    fbreak;
}

cr = '\r';
lf = '\n';
crlf = '\r\n';
tchar = alpha | digit | '-' | '!' | '#' | '$' | '%' | '&' | '\'' | '*'
        | '+' | '.' | '^' | '_' | '`' | '|' | '~';

sp = ' ';
ht = '\t';

sp_ht = sp | ht;

http_version = 'HTTP/' (digit '.' digit) >mark %store_version;

field = tchar+ >mark %store_field_name;
value = any* >mark %store_value;
start_line = http_version space digit digit digit space (any - cr - lf)* crlf;
header_1st = (field sp_ht* ':' value :> crlf) %assign_field;
header_cont = (sp_ht+ value sp_ht* crlf) %extend_field;
header = header_1st header_cont*;
main := start_line header* :> (crlf @done);

}%%

class http_response_parser : public ragel_parser_base<http_response_parser> {
    %% write data nofinal noprefix;
public:
    enum class state {
        error,
        eof,
        done,
    };
    std::unique_ptr<http_response> _rsp;
    sstring _field_name;
    sstring _value;
    state _state;
public:
    void init() {
        init_base();
        _rsp.reset(new http_response());
        _state = state::eof;
        %% write init;
    }
    char* parse(char* p, char* pe, char* eof) {
        sstring_builder::guard g(_builder, p, pe);
        auto str = [this, &g, &p] { g.mark_end(p); return get_str(); };
        bool done = false;
        if (p != pe) {
            _state = state::error;
        }
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wmisleading-indentation"
#endif
        %% write exec;
#ifdef __clang__
#pragma clang diagnostic pop
#endif
        if (!done) {
            p = nullptr;
        } else {
            _state = state::done;
        }
        return p;
    }
    auto get_parsed_response() {
        return std::move(_rsp);
    }
    bool eof() const {
        return _state == state::eof;
    }
};

}


Fixed octal sequence definition

Fixed octal sequence definition

%%{
  machine re_scanner;

  wild                  = '.';
  backslash             = '\\';
  alternation           = '|';
  beginning_of_line     = '^';
  end_of_line           = '$';

  range_open            = '{';
  range_close           = '}';
  curlies               = range_open | range_close;

  group_open            = '(';
  group_close           = ')';
  parantheses           = group_open | group_close;

  set_open              = '[';
  set_close             = ']';
  brackets              = set_open | set_close;

  posix_class_name      = 'alnum' | 'alpha' | 'blank' |
                          'cntrl' | 'digit' | 'graph' |
                          'lower' | 'print' | 'punct' |
                          'space' | 'upper' | 'xdigit' |
                          'word'  | 'ascii';

  property_name         = 'Alnum' | 'Alpha' | 'Any'   | 'Ascii' | 'Blank' |
                          'Cntrl' | 'Digit' | 'Graph' | 'Lower' | 'Print' |
                          'Punct' | 'Space' | 'Upper' | 'Word' | 'Xdigit';

  category_letter       = 'L' . [ultmo]?;
  category_mark         = 'M' . [nce]?;
  category_number       = 'N' . [dlo]?;
  category_punctuation  = 'P' . [cdseifo]?;
  category_symbol       = 'S' . [mcko]?;
  category_separator    = 'Z' . [slp]?;
  category_codepoint    = 'C' . [cfson]?;
  general_category      = category_letter | category_mark |
                          category_number | category_punctuation |
                          category_symbol | category_separator |
                          category_codepoint;

  posix_class           = '[:' . posix_class_name . ':]';

  char_type             = [dDhHsSwW];

  line_anchor           = beginning_of_line | end_of_line;
  anchor_char           = [AbBzZG];
  property_char         = [pP];

  escaped_char          = [aefnrtv];
  octal_sequence        = [0-7]{3};

  hex_sequence          = 'x' . xdigit{2};
  wide_hex_sequence     = 'x' . '{7' . xdigit{1,7} . '}';

  control_sequence      = ('c' | 'C-') . xdigit{1,2};
  meta_sequence         = 'M-' . xdigit{1,2};
  meta_control_sequence = 'M-\\C-' . xdigit{1,2};

  zero_or_one           = '?' | '??' | '?+';
  zero_or_more          = '*' | '*?' | '*+';
  one_or_more           = '+' | '+?' | '++';

  quantifier_greedy     = '?'  | '*'  | '+';
  quantifier_reluctant  = '??' | '*?' | '+?';
  quantifier_possessive = '?+' | '*+' | '++';
  quantifier_mode       = '?'  | '+';

  quantifier_range      = range_open . (digit+)? . ','? . (digit+)? .
                          range_close . quantifier_mode?;

  quantifiers           = quantifier_greedy | quantifier_reluctant |
                          quantifier_possessive | quantifier_range;

  group_comment         = '?#' . [^)]+ . group_close;

  group_atomic          = '?>';
  group_passive         = '?:';

  assertion_lookahead   = '?=';
  assertion_nlookahead  = '?!';
  assertion_lookbehind  = '?<=';
  assertion_nlookbehind = '?<!';

  group_options         = '?' . ([mix]{1,3})? . '-' . ([mix]{1,3})? . ':'?;

  group_name            = alpha . alnum+;
  group_named           = ('?<' . group_name . '>') | ('?\'' . group_name . '\'');

  group_type            = group_atomic | group_passive | group_named;

  assertion_type        = assertion_lookahead  | assertion_nlookahead |
                          assertion_lookbehind | assertion_nlookbehind;

  # characters that 'break' a literal
  meta_char             = wild | backslash | alternation |
                          curlies | parantheses | brackets |
                          line_anchor | quantifier_greedy;

  # Character set scanner, continues consuming characters until it meets the
  # closing bracket of the set.
  # --------------------------------------------------------------------------
  character_set := |*
    ']' {
      self.emit(:set, :close, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    '^' {
      self.emit(:set, :negate, data[ts..te-1].pack('c*'), ts, te)
    };

    alnum . '-' . alnum { # TODO: add properties
      self.emit(:set, :range, data[ts..te-1].pack('c*'), ts, te)
    };

    '&&' {
      self.emit(:set, :intersection, data[ts..te-1].pack('c*'), ts, te)
    };

    '\\' {
      fcall set_escape_sequence;
    };

    posix_class {
      case text = data[ts..te-1].pack('c*')
      when '[:alnum:]';  self.emit(:set, :class_alnum,  text, ts, te)
      when '[:alpha:]';  self.emit(:set, :class_alpha,  text, ts, te)
      when '[:blank:]';  self.emit(:set, :class_blank,  text, ts, te)
      when '[:cntrl:]';  self.emit(:set, :class_cntrl,  text, ts, te)
      when '[:digit:]';  self.emit(:set, :class_digit,  text, ts, te)
      when '[:graph:]';  self.emit(:set, :class_graph,  text, ts, te)
      when '[:lower:]';  self.emit(:set, :class_lower,  text, ts, te)
      when '[:print:]';  self.emit(:set, :class_print,  text, ts, te)
      when '[:punct:]';  self.emit(:set, :class_punct,  text, ts, te)
      when '[:space:]';  self.emit(:set, :class_space,  text, ts, te)
      when '[:upper:]';  self.emit(:set, :class_upper,  text, ts, te)
      when '[:xdigit:]'; self.emit(:set, :class_xdigit, text, ts, te)
      when '[:word:]';   self.emit(:set, :class_word,   text, ts, te)
      when '[:ascii:]';  self.emit(:set, :class_ascii,  text, ts, te)
      else raise "Unsupported character posixe class at #{text} (char #{ts})"
      end
    };

    any {
      self.emit(:set, :member, data[ts..te-1].pack('c*'), ts, te)
    };
  *|;

  # set escapes scanner
  # --------------------------------------------------------------------------
  set_escape_sequence := |*
    'b' {
      self.emit(:set, :backspace, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    [\\\]\-\,] {
      self.emit(:set, :escape, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    [dDhHsSwW] {
      case text = data[ts-1..te-1].pack('c*')
      when '\d'; self.emit(:set, :type_digit,     text, ts-1, te)
      when '\D'; self.emit(:set, :type_nondigit,  text, ts-1, te)
      when '\h'; self.emit(:set, :type_hex,       text, ts-1, te)
      when '\H'; self.emit(:set, :type_nonhex,    text, ts-1, te)
      when '\s'; self.emit(:set, :type_space,     text, ts-1, te)
      when '\S'; self.emit(:set, :type_nonspace,  text, ts-1, te)
      when '\w'; self.emit(:set, :type_word,      text, ts-1, te)
      when '\W'; self.emit(:set, :type_nonword,   text, ts-1, te)
      end

      fret;
    };

    hex_sequence . '-\\' . hex_sequence {
      self.emit(:set, :range_hex, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    hex_sequence {
      self.emit(:set, :member_hex, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };
  *|;

  # escape sequence scanner
  # --------------------------------------------------------------------------
  escape_sequence := |*
    hex_sequence {
      self.emit(:escape, :hex, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    wide_hex_sequence {
      self.emit(:escape, :hex_wide, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    control_sequence {
      self.emit(:escape, :control, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    meta_sequence {
      self.emit(:escape, :meta, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    meta_control_sequence {
      self.emit(:escape, :meta_control, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    # TODO: extract into a separate machine... use in sets
    property_char . '{' . (property_name | general_category) . '}' > (escaped_alpha, 2) {
      text = data[ts..te-1].pack('c*')
      type = text[0,1] == 'p' ? :property: :inverted_property

      case name = data[ts+2..te-2].pack('c*')

      # Named
      when 'Alnum';   self.emit(type, :alnum,  text, ts, te)
      when 'Alpha';   self.emit(type, :alpha,  text, ts, te)
      when 'Any';     self.emit(type, :any,    text, ts, te)
      when 'Ascii';   self.emit(type, :ascii,  text, ts, te)
      when 'Blank';   self.emit(type, :blank,  text, ts, te)
      when 'Cntrl';   self.emit(type, :cntrl,  text, ts, te)
      when 'Digit';   self.emit(type, :digit,  text, ts, te)
      when 'Graph';   self.emit(type, :graph,  text, ts, te)
      when 'Lower';   self.emit(type, :lower,  text, ts, te)
      when 'Print';   self.emit(type, :print,  text, ts, te)
      when 'Punct';   self.emit(type, :punct,  text, ts, te)
      when 'Space';   self.emit(type, :space,  text, ts, te)
      when 'Upper';   self.emit(type, :upper,  text, ts, te)
      when 'Word';    self.emit(type, :word,   text, ts, te)
      when 'Xdigit';  self.emit(type, :xdigit, text, ts, te)

      # Letters
      when 'L';  self.emit(type, :letter_any,       text, ts, te)
      when 'Lu'; self.emit(type, :letter_uppercase, text, ts, te)
      when 'Ll'; self.emit(type, :letter_lowercase, text, ts, te)
      when 'Lt'; self.emit(type, :letter_titlecase, text, ts, te)
      when 'Lm'; self.emit(type, :letter_modifier,  text, ts, te)
      when 'Lo'; self.emit(type, :letter_other,     text, ts, te)

      # Marks
      when 'M';  self.emit(type, :mark_any,         text, ts, te)
      when 'Mn'; self.emit(type, :mark_nonspacing,  text, ts, te)
      when 'Mc'; self.emit(type, :mark_spacing,     text, ts, te)
      when 'Me'; self.emit(type, :mark_enclosing,   text, ts, te)

      # Numbers
      when 'N';  self.emit(type, :number_any,       text, ts, te)
      when 'Nd'; self.emit(type, :number_decimal,   text, ts, te)
      when 'Nl'; self.emit(type, :number_letter,    text, ts, te)
      when 'No'; self.emit(type, :number_other,     text, ts, te)

      # Punctuation
      when 'P';  self.emit(type, :punct_any,        text, ts, te)
      when 'Pc'; self.emit(type, :punct_connector,  text, ts, te)
      when 'Pd'; self.emit(type, :punct_dash,       text, ts, te)
      when 'Ps'; self.emit(type, :punct_open,       text, ts, te)
      when 'Pe'; self.emit(type, :punct_close,      text, ts, te)
      when 'Pi'; self.emit(type, :punct_initial,    text, ts, te)
      when 'Pf'; self.emit(type, :punct_final,      text, ts, te)
      when 'Po'; self.emit(type, :punct_other,      text, ts, te)

      # Symbols
      when 'S';  self.emit(type, :symbol_any,       text, ts, te)
      when 'Sm'; self.emit(type, :symbol_math,      text, ts, te)
      when 'Sc'; self.emit(type, :symbol_currency,  text, ts, te)
      when 'Sk'; self.emit(type, :symbol_modifier,  text, ts, te)
      when 'So'; self.emit(type, :symbol_other,     text, ts, te)

      # Separators
      when 'Z';  self.emit(type, :separator_any,        text, ts, te)
      when 'Zs'; self.emit(type, :separator_space,      text, ts, te)
      when 'Zl'; self.emit(type, :separator_line,       text, ts, te)
      when 'Zp'; self.emit(type, :separator_paragraph,  text, ts, te)

      # Codepoints
      when 'C';  self.emit(type, :code_any,         text, ts, te)
      when 'Cc'; self.emit(type, :code_control,     text, ts, te)
      when 'Cf'; self.emit(type, :code_format,      text, ts, te)
      when 'Cs'; self.emit(type, :code_surrogate,   text, ts, te)
      when 'Co'; self.emit(type, :code_private,     text, ts, te)
      when 'Cn'; self.emit(type, :code_unassigned,  text, ts, te)
      end
    };

    any > (escaped_alpha, 1)  {
      self.emit(:escape, :literal, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };
  *|;

  # Main scanner
  # --------------------------------------------------------------------------
  main := |*

    # Meta characters
    alternation {
      self.emit(:meta, :alternation, data[ts..te-1].pack('c*'), ts, te)
    };

    wild {
      self.emit(:meta, :wild, data[ts..te-1].pack('c*'), ts, te)
    };

    # Character types
    #   \d, \D    digit, non-digit
    #   \h, \H    hex, non-hex
    #   \s, \S    whitespace, non-whitespace
    #   \w, \W    word, non-word
    # ------------------------------------------------------------------------
    backslash . char_type > (backslashed, 2) {
      case text = data[ts..te-1].pack('c*')
      when '\\d'; self.emit(:type, :digit,      text, ts, te)
      when '\\D'; self.emit(:type, :nondigit,   text, ts, te)
      when '\\h'; self.emit(:type, :hex,        text, ts, te)
      when '\\H'; self.emit(:type, :nonhex,     text, ts, te)
      when '\\s'; self.emit(:type, :space,      text, ts, te)
      when '\\S'; self.emit(:type, :nonspace,   text, ts, te)
      when '\\w'; self.emit(:type, :word,       text, ts, te)
      when '\\W'; self.emit(:type, :nonword,    text, ts, te)
      end
    };

    # Anchors
    beginning_of_line {
      self.emit(:anchor, :beginning_of_line, data[ts..te-1].pack('c*'), ts, te)
    };

    end_of_line {
      self.emit(:anchor, :end_of_line, data[ts..te-1].pack('c*'), ts, te)
    };

    backslash . anchor_char > (backslashed, 3) {
      case text = data[ts..te-1].pack('c*')
      when '\\A'; self.emit(:anchor, :bos,                text, ts, te)
      when '\\z'; self.emit(:anchor, :eos,                text, ts, te)
      when '\\Z'; self.emit(:anchor, :eos_or_before_eol,  text, ts, te)
      when '\\b'; self.emit(:anchor, :word_boundary,      text, ts, te)
      when '\\B'; self.emit(:anchor, :nonword_boundary,   text, ts, te)
      else raise "Unsupported anchor at #{text} (char #{ts})"
      end
    };

    # Escaped sequences
    backslash > (backslashed, 1) {
      fcall escape_sequence;
    };

    # Character sets
    set_open {
      self.emit(:set, :open, data[ts..te-1].pack('c*'), ts, te)
      fcall character_set;
    };

    # (?#...) comments: parsed as a single expression, without introducing a
    # new nesting level. Comments may not include parentheses, escaped or not.
    group_open . group_comment {
      self.emit(:group, :comment, data[ts..te-1].pack('c*'), ts, te)
    };

    # (?mix-mix...) expression options:
    #   (?imx-imx)          option on/off
    #                         i: ignore case
    #                         m: multi-line (dot(.) match newline)
    #                         x: extended form
    #
    #   (?imx-imx:subexp)   option on/off for subexp
    group_open . group_options {
      self.emit(:group, :options, data[ts..te-1].pack('c*'), ts, te)
    };

    # Assertions
    #   (?=subexp)          look-ahead
    #   (?!subexp)          negative look-ahead
    #   (?<=subexp)         look-behind
    #   (?<!subexp)         negative look-behind
    # ------------------------------------------------------------------------
    group_open . assertion_type {
      case text =  data[ts..te-1].pack('c*')
      when '(?=';  self.emit(:assertion, :lookahead,    text, ts, te)
      when '(?!';  self.emit(:assertion, :nlookahead,   text, ts, te)
      when '(?<='; self.emit(:assertion, :lookbehind,   text, ts, te)
      when '(?<!'; self.emit(:assertion, :nlookbehind,  text, ts, te)
      end
    };

    # Groups
    #   (?:subexp)          passive (non-captured) group
    #   (?>subexp)          atomic group, don't backtrack in subexp.
    #   (?<name>subexp)     named group (single quotes are no supported, yet)
    #   (subexp)            captured group
    # ------------------------------------------------------------------------
    group_open . group_type {
      case text =  data[ts..te-1].pack('c*')
      when '(?:';  self.emit(:group, :passive,      text, ts, te)
      when '(?>';  self.emit(:group, :atomic,       text, ts, te)
      when /\(\?<\w+>/
        self.emit(:group, :named, text, ts, te)
      end
    };

    group_open  {
      text =  data[ts..te-1].pack('c*')
      self.emit(:group, :capture, text, ts, te)
    };

    group_close {
      self.emit(:group, :close, data[ts..te-1].pack('c*'), ts, te)
    };

    # Quantifiers
    # ------------------------------------------------------------------------
    zero_or_one {
      case text =  data[ts..te-1].pack('c*')
      when '?' ;  self.emit(:quantifier, :zero_or_one,            text, ts, te)
      when '??';  self.emit(:quantifier, :zero_or_one_reluctant,  text, ts, te)
      when '?+';  self.emit(:quantifier, :zero_or_one_possessive, text, ts, te)
      end
    };

    zero_or_more {
      case text =  data[ts..te-1].pack('c*')
      when '*' ;  self.emit(:quantifier, :zero_or_more,            text, ts, te)
      when '*?';  self.emit(:quantifier, :zero_or_more_reluctant,  text, ts, te)
      when '*+';  self.emit(:quantifier, :zero_or_more_possessive, text, ts, te)
      end
    };

    one_or_more {
      case text =  data[ts..te-1].pack('c*')
      when '+' ;  self.emit(:quantifier, :one_or_more,            text, ts, te)
      when '+?';  self.emit(:quantifier, :one_or_more_reluctant,  text, ts, te)
      when '++';  self.emit(:quantifier, :one_or_more_possessive, text, ts, te)
      end
    };

    # Intervals: min, max, and exact notations
    # ------------------------------------------------------------------------
    range_open . (digit+)? . ','? . (digit+)? . range_close . quantifier_mode? {
      self.emit(:quantifier, :interval, data[ts..te-1].pack('c*'), ts, te)
    };

    # Literal: anything, except meta characters. This includes 2, 3, and 4
    # unicode byte sequences.
    # ------------------------------------------------------------------------
    (any - meta_char)+ {
      self.emit(:literal, :literal, data[ts..te-1].pack('c*'), ts, te)
    };

  *|;
}%%

%%{
  machine re_scanner;

  wild                  = '.';
  backslash             = '\\';
  alternation           = '|';
  beginning_of_line     = '^';
  end_of_line           = '$';

  range_open            = '{';
  range_close           = '}';
  curlies               = range_open | range_close;

  group_open            = '(';
  group_close           = ')';
  parantheses           = group_open | group_close;

  set_open              = '[';
  set_close             = ']';
  brackets              = set_open | set_close;

  posix_class_name      = 'alnum' | 'alpha' | 'blank' |
                          'cntrl' | 'digit' | 'graph' |
                          'lower' | 'print' | 'punct' |
                          'space' | 'upper' | 'xdigit' |
                          'word'  | 'ascii';

  property_name         = 'Alnum' | 'Alpha' | 'Any'   | 'Ascii' | 'Blank' |
                          'Cntrl' | 'Digit' | 'Graph' | 'Lower' | 'Print' |
                          'Punct' | 'Space' | 'Upper' | 'Word' | 'Xdigit';

  category_letter       = 'L' . [ultmo]?;
  category_mark         = 'M' . [nce]?;
  category_number       = 'N' . [dlo]?;
  category_punctuation  = 'P' . [cdseifo]?;
  category_symbol       = 'S' . [mcko]?;
  category_separator    = 'Z' . [slp]?;
  category_codepoint    = 'C' . [cfson]?;
  general_category      = category_letter | category_mark |
                          category_number | category_punctuation |
                          category_symbol | category_separator |
                          category_codepoint;

  posix_class           = '[:' . posix_class_name . ':]';

  char_type             = [dDhHsSwW];

  line_anchor           = beginning_of_line | end_of_line;
  anchor_char           = [AbBzZG];
  property_char         = [pP];

  escaped_char          = [aefnrtv];
  octal_sequence        = '0' . [0-7]{2};

  hex_sequence          = 'x' . xdigit{2};
  wide_hex_sequence     = 'x' . '{7' . xdigit{1,7} . '}';

  control_sequence      = ('c' | 'C-') . xdigit{1,2};
  meta_sequence         = 'M-' . xdigit{1,2};
  meta_control_sequence = 'M-\\C-' . xdigit{1,2};

  zero_or_one           = '?' | '??' | '?+';
  zero_or_more          = '*' | '*?' | '*+';
  one_or_more           = '+' | '+?' | '++';

  quantifier_greedy     = '?'  | '*'  | '+';
  quantifier_reluctant  = '??' | '*?' | '+?';
  quantifier_possessive = '?+' | '*+' | '++';
  quantifier_mode       = '?'  | '+';

  quantifier_range      = range_open . (digit+)? . ','? . (digit+)? .
                          range_close . quantifier_mode?;

  quantifiers           = quantifier_greedy | quantifier_reluctant |
                          quantifier_possessive | quantifier_range;

  group_comment         = '?#' . [^)]+ . group_close;

  group_atomic          = '?>';
  group_passive         = '?:';

  assertion_lookahead   = '?=';
  assertion_nlookahead  = '?!';
  assertion_lookbehind  = '?<=';
  assertion_nlookbehind = '?<!';

  group_options         = '?' . ([mix]{1,3})? . '-' . ([mix]{1,3})? . ':'?;

  group_name            = alpha . alnum+;
  group_named           = ('?<' . group_name . '>') | ('?\'' . group_name . '\'');

  group_type            = group_atomic | group_passive | group_named;

  assertion_type        = assertion_lookahead  | assertion_nlookahead |
                          assertion_lookbehind | assertion_nlookbehind;

  # characters that 'break' a literal
  meta_char             = wild | backslash | alternation |
                          curlies | parantheses | brackets |
                          line_anchor | quantifier_greedy;

  # Character set scanner, continues consuming characters until it meets the
  # closing bracket of the set.
  # --------------------------------------------------------------------------
  character_set := |*
    ']' {
      self.emit(:set, :close, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    '^' {
      self.emit(:set, :negate, data[ts..te-1].pack('c*'), ts, te)
    };

    alnum . '-' . alnum { # TODO: add properties
      self.emit(:set, :range, data[ts..te-1].pack('c*'), ts, te)
    };

    '&&' {
      self.emit(:set, :intersection, data[ts..te-1].pack('c*'), ts, te)
    };

    '\\' {
      fcall set_escape_sequence;
    };

    posix_class {
      case text = data[ts..te-1].pack('c*')
      when '[:alnum:]';  self.emit(:set, :class_alnum,  text, ts, te)
      when '[:alpha:]';  self.emit(:set, :class_alpha,  text, ts, te)
      when '[:blank:]';  self.emit(:set, :class_blank,  text, ts, te)
      when '[:cntrl:]';  self.emit(:set, :class_cntrl,  text, ts, te)
      when '[:digit:]';  self.emit(:set, :class_digit,  text, ts, te)
      when '[:graph:]';  self.emit(:set, :class_graph,  text, ts, te)
      when '[:lower:]';  self.emit(:set, :class_lower,  text, ts, te)
      when '[:print:]';  self.emit(:set, :class_print,  text, ts, te)
      when '[:punct:]';  self.emit(:set, :class_punct,  text, ts, te)
      when '[:space:]';  self.emit(:set, :class_space,  text, ts, te)
      when '[:upper:]';  self.emit(:set, :class_upper,  text, ts, te)
      when '[:xdigit:]'; self.emit(:set, :class_xdigit, text, ts, te)
      when '[:word:]';   self.emit(:set, :class_word,   text, ts, te)
      when '[:ascii:]';  self.emit(:set, :class_ascii,  text, ts, te)
      else raise "Unsupported character posixe class at #{text} (char #{ts})"
      end
    };

    any {
      self.emit(:set, :member, data[ts..te-1].pack('c*'), ts, te)
    };
  *|;

  # set escapes scanner
  # --------------------------------------------------------------------------
  set_escape_sequence := |*
    'b' {
      self.emit(:set, :backspace, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    [\\\]\-\,] {
      self.emit(:set, :escape, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    [dDhHsSwW] {
      case text = data[ts-1..te-1].pack('c*')
      when '\d'; self.emit(:set, :type_digit,     text, ts-1, te)
      when '\D'; self.emit(:set, :type_nondigit,  text, ts-1, te)
      when '\h'; self.emit(:set, :type_hex,       text, ts-1, te)
      when '\H'; self.emit(:set, :type_nonhex,    text, ts-1, te)
      when '\s'; self.emit(:set, :type_space,     text, ts-1, te)
      when '\S'; self.emit(:set, :type_nonspace,  text, ts-1, te)
      when '\w'; self.emit(:set, :type_word,      text, ts-1, te)
      when '\W'; self.emit(:set, :type_nonword,   text, ts-1, te)
      end

      fret;
    };

    hex_sequence . '-\\' . hex_sequence {
      self.emit(:set, :range_hex, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };

    hex_sequence {
      self.emit(:set, :member_hex, data[ts-1..te-1].pack('c*'), ts-1, te)
      fret;
    };
  *|;

  # escape sequence scanner
  # --------------------------------------------------------------------------
  escape_sequence := |*
    hex_sequence {
      self.emit(:escape, :hex, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    wide_hex_sequence {
      self.emit(:escape, :hex_wide, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    control_sequence {
      self.emit(:escape, :control, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    meta_sequence {
      self.emit(:escape, :meta, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    meta_control_sequence {
      self.emit(:escape, :meta_control, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };

    # TODO: extract into a separate machine... use in sets
    property_char . '{' . (property_name | general_category) . '}' > (escaped_alpha, 2) {
      text = data[ts..te-1].pack('c*')
      type = text[0,1] == 'p' ? :property: :inverted_property

      case name = data[ts+2..te-2].pack('c*')

      # Named
      when 'Alnum';   self.emit(type, :alnum,  text, ts, te)
      when 'Alpha';   self.emit(type, :alpha,  text, ts, te)
      when 'Any';     self.emit(type, :any,    text, ts, te)
      when 'Ascii';   self.emit(type, :ascii,  text, ts, te)
      when 'Blank';   self.emit(type, :blank,  text, ts, te)
      when 'Cntrl';   self.emit(type, :cntrl,  text, ts, te)
      when 'Digit';   self.emit(type, :digit,  text, ts, te)
      when 'Graph';   self.emit(type, :graph,  text, ts, te)
      when 'Lower';   self.emit(type, :lower,  text, ts, te)
      when 'Print';   self.emit(type, :print,  text, ts, te)
      when 'Punct';   self.emit(type, :punct,  text, ts, te)
      when 'Space';   self.emit(type, :space,  text, ts, te)
      when 'Upper';   self.emit(type, :upper,  text, ts, te)
      when 'Word';    self.emit(type, :word,   text, ts, te)
      when 'Xdigit';  self.emit(type, :xdigit, text, ts, te)

      # Letters
      when 'L';  self.emit(type, :letter_any,       text, ts, te)
      when 'Lu'; self.emit(type, :letter_uppercase, text, ts, te)
      when 'Ll'; self.emit(type, :letter_lowercase, text, ts, te)
      when 'Lt'; self.emit(type, :letter_titlecase, text, ts, te)
      when 'Lm'; self.emit(type, :letter_modifier,  text, ts, te)
      when 'Lo'; self.emit(type, :letter_other,     text, ts, te)

      # Marks
      when 'M';  self.emit(type, :mark_any,         text, ts, te)
      when 'Mn'; self.emit(type, :mark_nonspacing,  text, ts, te)
      when 'Mc'; self.emit(type, :mark_spacing,     text, ts, te)
      when 'Me'; self.emit(type, :mark_enclosing,   text, ts, te)

      # Numbers
      when 'N';  self.emit(type, :number_any,       text, ts, te)
      when 'Nd'; self.emit(type, :number_decimal,   text, ts, te)
      when 'Nl'; self.emit(type, :number_letter,    text, ts, te)
      when 'No'; self.emit(type, :number_other,     text, ts, te)

      # Punctuation
      when 'P';  self.emit(type, :punct_any,        text, ts, te)
      when 'Pc'; self.emit(type, :punct_connector,  text, ts, te)
      when 'Pd'; self.emit(type, :punct_dash,       text, ts, te)
      when 'Ps'; self.emit(type, :punct_open,       text, ts, te)
      when 'Pe'; self.emit(type, :punct_close,      text, ts, te)
      when 'Pi'; self.emit(type, :punct_initial,    text, ts, te)
      when 'Pf'; self.emit(type, :punct_final,      text, ts, te)
      when 'Po'; self.emit(type, :punct_other,      text, ts, te)

      # Symbols
      when 'S';  self.emit(type, :symbol_any,       text, ts, te)
      when 'Sm'; self.emit(type, :symbol_math,      text, ts, te)
      when 'Sc'; self.emit(type, :symbol_currency,  text, ts, te)
      when 'Sk'; self.emit(type, :symbol_modifier,  text, ts, te)
      when 'So'; self.emit(type, :symbol_other,     text, ts, te)

      # Separators
      when 'Z';  self.emit(type, :separator_any,        text, ts, te)
      when 'Zs'; self.emit(type, :separator_space,      text, ts, te)
      when 'Zl'; self.emit(type, :separator_line,       text, ts, te)
      when 'Zp'; self.emit(type, :separator_paragraph,  text, ts, te)

      # Codepoints
      when 'C';  self.emit(type, :code_any,         text, ts, te)
      when 'Cc'; self.emit(type, :code_control,     text, ts, te)
      when 'Cf'; self.emit(type, :code_format,      text, ts, te)
      when 'Cs'; self.emit(type, :code_surrogate,   text, ts, te)
      when 'Co'; self.emit(type, :code_private,     text, ts, te)
      when 'Cn'; self.emit(type, :code_unassigned,  text, ts, te)
      end
    };

    any > (escaped_alpha, 1)  {
      self.emit(:escape, :literal, data[ts..te-1].pack('c*'), ts, te)
      fret;
    };
  *|;

  # Main scanner
  # --------------------------------------------------------------------------
  main := |*

    # Meta characters
    alternation {
      self.emit(:meta, :alternation, data[ts..te-1].pack('c*'), ts, te)
    };

    wild {
      self.emit(:meta, :wild, data[ts..te-1].pack('c*'), ts, te)
    };

    # Character types
    #   \d, \D    digit, non-digit
    #   \h, \H    hex, non-hex
    #   \s, \S    whitespace, non-whitespace
    #   \w, \W    word, non-word
    # ------------------------------------------------------------------------
    backslash . char_type > (backslashed, 2) {
      case text = data[ts..te-1].pack('c*')
      when '\\d'; self.emit(:type, :digit,      text, ts, te)
      when '\\D'; self.emit(:type, :nondigit,   text, ts, te)
      when '\\h'; self.emit(:type, :hex,        text, ts, te)
      when '\\H'; self.emit(:type, :nonhex,     text, ts, te)
      when '\\s'; self.emit(:type, :space,      text, ts, te)
      when '\\S'; self.emit(:type, :nonspace,   text, ts, te)
      when '\\w'; self.emit(:type, :word,       text, ts, te)
      when '\\W'; self.emit(:type, :nonword,    text, ts, te)
      end
    };

    # Anchors
    beginning_of_line {
      self.emit(:anchor, :beginning_of_line, data[ts..te-1].pack('c*'), ts, te)
    };

    end_of_line {
      self.emit(:anchor, :end_of_line, data[ts..te-1].pack('c*'), ts, te)
    };

    backslash . anchor_char > (backslashed, 3) {
      case text = data[ts..te-1].pack('c*')
      when '\\A'; self.emit(:anchor, :bos,                text, ts, te)
      when '\\z'; self.emit(:anchor, :eos,                text, ts, te)
      when '\\Z'; self.emit(:anchor, :eos_or_before_eol,  text, ts, te)
      when '\\b'; self.emit(:anchor, :word_boundary,      text, ts, te)
      when '\\B'; self.emit(:anchor, :nonword_boundary,   text, ts, te)
      else raise "Unsupported anchor at #{text} (char #{ts})"
      end
    };

    # Escaped sequences
    backslash > (backslashed, 1) {
      fcall escape_sequence;
    };

    # Character sets
    set_open {
      self.emit(:set, :open, data[ts..te-1].pack('c*'), ts, te)
      fcall character_set;
    };

    # (?#...) comments: parsed as a single expression, without introducing a
    # new nesting level. Comments may not include parentheses, escaped or not.
    group_open . group_comment {
      self.emit(:group, :comment, data[ts..te-1].pack('c*'), ts, te)
    };

    # (?mix-mix...) expression options:
    #   (?imx-imx)          option on/off
    #                         i: ignore case
    #                         m: multi-line (dot(.) match newline)
    #                         x: extended form
    #
    #   (?imx-imx:subexp)   option on/off for subexp
    group_open . group_options {
      self.emit(:group, :options, data[ts..te-1].pack('c*'), ts, te)
    };

    # Assertions
    #   (?=subexp)          look-ahead
    #   (?!subexp)          negative look-ahead
    #   (?<=subexp)         look-behind
    #   (?<!subexp)         negative look-behind
    # ------------------------------------------------------------------------
    group_open . assertion_type {
      case text =  data[ts..te-1].pack('c*')
      when '(?=';  self.emit(:assertion, :lookahead,    text, ts, te)
      when '(?!';  self.emit(:assertion, :nlookahead,   text, ts, te)
      when '(?<='; self.emit(:assertion, :lookbehind,   text, ts, te)
      when '(?<!'; self.emit(:assertion, :nlookbehind,  text, ts, te)
      end
    };

    # Groups
    #   (?:subexp)          passive (non-captured) group
    #   (?>subexp)          atomic group, don't backtrack in subexp.
    #   (?<name>subexp)     named group (single quotes are no supported, yet)
    #   (subexp)            captured group
    # ------------------------------------------------------------------------
    group_open . group_type {
      case text =  data[ts..te-1].pack('c*')
      when '(?:';  self.emit(:group, :passive,      text, ts, te)
      when '(?>';  self.emit(:group, :atomic,       text, ts, te)
      when /\(\?<\w+>/
        self.emit(:group, :named, text, ts, te)
      end
    };

    group_open  {
      text =  data[ts..te-1].pack('c*')
      self.emit(:group, :capture, text, ts, te)
    };

    group_close {
      self.emit(:group, :close, data[ts..te-1].pack('c*'), ts, te)
    };

    # Quantifiers
    # ------------------------------------------------------------------------
    zero_or_one {
      case text =  data[ts..te-1].pack('c*')
      when '?' ;  self.emit(:quantifier, :zero_or_one,            text, ts, te)
      when '??';  self.emit(:quantifier, :zero_or_one_reluctant,  text, ts, te)
      when '?+';  self.emit(:quantifier, :zero_or_one_possessive, text, ts, te)
      end
    };

    zero_or_more {
      case text =  data[ts..te-1].pack('c*')
      when '*' ;  self.emit(:quantifier, :zero_or_more,            text, ts, te)
      when '*?';  self.emit(:quantifier, :zero_or_more_reluctant,  text, ts, te)
      when '*+';  self.emit(:quantifier, :zero_or_more_possessive, text, ts, te)
      end
    };

    one_or_more {
      case text =  data[ts..te-1].pack('c*')
      when '+' ;  self.emit(:quantifier, :one_or_more,            text, ts, te)
      when '+?';  self.emit(:quantifier, :one_or_more_reluctant,  text, ts, te)
      when '++';  self.emit(:quantifier, :one_or_more_possessive, text, ts, te)
      end
    };

    # Intervals: min, max, and exact notations
    # ------------------------------------------------------------------------
    range_open . (digit+)? . ','? . (digit+)? . range_close . quantifier_mode? {
      self.emit(:quantifier, :interval, data[ts..te-1].pack('c*'), ts, te)
    };

    # Literal: anything, except meta characters. This includes 2, 3, and 4
    # unicode byte sequences.
    # ------------------------------------------------------------------------
    (any - meta_char)+ {
      self.emit(:literal, :literal, data[ts..te-1].pack('c*'), ts, te)
    };

  *|;
}%%


Add a CONFIG_RL_DEBUG define that prints debugging messages for the crontab parser.

/* config.rl - configure file parser for ncron
 *
 * (c) 2003-2014 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 <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.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>
#include "nk/log.h"
#include "nk/malloc.h"
#include "nk/privilege.h"

#include "ncron.h"
#include "config.h"
#include "sched.h"

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

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

struct ParseCfgState
{
    char v_str[1024];

    cronentry_t *stack;
    cronentry_t *deadstack;
    cronentry_t *ce;

    char *execfile;

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

    size_t v_strlen;
    size_t linenum;

    unsigned int v_jobid;
    unsigned int v_time;

    int v_int;
    int v_int2;

    int cs;
    int noextime;
    int cmdret;

    bool intv2_exist;
};

static void nullify_item(cronentry_t *item)
{
    if (!item)
        return;
    item->id = 0;
    item->command = NULL;
    item->args = NULL;
    item->chroot = NULL;
    item->numruns = 0;
    item->maxruns = 0;
    item->journal = 0;
    item->user = 0;
    item->group = 0;
    item->month = NULL;
    item->day = NULL;
    item->weekday = NULL;
    item->hour = NULL;
    item->minute = NULL;
    item->interval = 0;
    item->exectime = 0;
    item->lasttime = 0;
    item->limits = NULL;
    item->next = NULL;
}

static void nullify_limits(limit_t *l)
{
    if (!l)
        return;
    l->cpu = NULL;
    l->fsize = NULL;
    l->data = NULL;
    l->stack = NULL;
    l->core = NULL;
    l->rss = NULL;
    l->nproc = NULL;
    l->nofile = NULL;
    l->memlock = NULL;
    l->as = NULL;
}

struct hstm {
    char *st;
    int cs;
    int id;
    time_t exectime;
    time_t lasttime;
    unsigned int numruns;
    bool got_exectime:1;
    bool got_lasttime:1;
    bool got_numruns:1;
};

%%{
    machine history_m;
    access hstm->;

    action St { hstm->st = p; }
    action LastTimeEn {
        hstm->lasttime = atoi(p);
        hstm->got_lasttime = 1;
    }
    action NumRunsEn {
        hstm->numruns = atoi(p);
        hstm->got_numruns = 1;
    }
    action ExecTimeEn {
        hstm->exectime = atoi(p);
        hstm->got_exectime = 1;
    }
    action IdEn { hstm->id = atoi(p); }

    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_get_history(struct hstm *hstm, char *buf, size_t blen)
{
    char *p = buf;
    const char *pe = buf + blen;
    const char *eof = pe;

    %% write init;
    %% write exec;

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

static void get_history(cronentry_t *item, char *path, int noextime)
{
    struct hstm hstm = {0};

    assert(item);

    if (!noextime && !hstm.got_exectime)
        item->exectime = 0;

    FILE *f = fopen(path, "r");
    if (!f) {
        log_line("failed to open history file \"%s\" for read", path);
        if (!noextime)
            item->exectime = (time_t)0; /* gracefully fail */
        return;
    }

    char buf[MAXLINE];
    while (fgets(buf, sizeof buf, f)) {
        int r = do_get_history(&hstm, buf, strlen(buf));
        if (r < 0)
            suicide("%s: do_get_history(%s) failed", __func__, path);
        if (r == 1) {
            if (hstm.id == item->id) {
                if (hstm.got_lasttime)
                    item->lasttime = hstm.lasttime;
                if (hstm.got_numruns)
                    item->numruns = hstm.numruns;
                if (hstm.got_exectime && !noextime)
                    item->exectime = hstm.exectime;
                break;
            }
        }
    }
    if (fclose(f))
        suicide("%s: fclose(%s) failed: %s", __func__, path, strerror(errno));
}

static void setlim(struct ParseCfgState *ncs, int type)
{
    struct rlimit **p = NULL;
    if (!ncs->ce->limits) {
        ncs->ce->limits = xmalloc(sizeof(limit_t));
        nullify_limits(ncs->ce->limits);
    }
    switch (type) {
    case RLIMIT_CPU: *p = ncs->ce->limits->cpu; break;
    case RLIMIT_FSIZE: *p = ncs->ce->limits->fsize; break;
    case RLIMIT_DATA: *p = ncs->ce->limits->data; break;
    case RLIMIT_STACK: *p = ncs->ce->limits->stack; break;
    case RLIMIT_CORE: *p = ncs->ce->limits->core; break;
    case RLIMIT_RSS: *p = ncs->ce->limits->rss; break;
    case RLIMIT_NPROC: *p = ncs->ce->limits->nproc; break;
    case RLIMIT_NOFILE: *p = ncs->ce->limits->nofile; break;
    case RLIMIT_MEMLOCK: *p = ncs->ce->limits->memlock; break;
#ifndef BSD
    case RLIMIT_AS: *p = ncs->ce->limits->as; break;
#endif /* BSD */
    default: return;
    }
    if (!p)
        suicide("%s: unexpected NULL, corruption?", __func__);
    if (!*p)
        *p = xmalloc(sizeof(struct rlimit));
    if (ncs->v_int == 0) (*p)->rlim_cur = RLIM_INFINITY;
    else (*p)->rlim_cur = ncs->v_int;

    if (ncs->v_int2 == 0) (*p)->rlim_max = RLIM_INFINITY;
    else (*p)->rlim_cur = ncs->v_int2;
}

static void addipairlist(struct ParseCfgState *ncs,
                         ipair_node_t **list, int wildcard, int min, int max)
{
    ipair_node_t *l;
    int low, high = wildcard;

    low = ncs->v_int;
    if (ncs->intv2_exist)
        high = ncs->v_int2;

    if (high == wildcard)
        high = low;
    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 (*list == NULL) {
        *list = xmalloc(sizeof(ipair_node_t));
        l = *list;
    } else {
        l = *list;
        while (l->next)
            l = l->next;

        l->next = xmalloc(sizeof(ipair_node_t));
        l = l->next;
    }

    /* discontinuous range, split into two continuous rules... */
    if (low > high) {
        l->node.l = low;
        l->node.h = max;
        l->next = xmalloc(sizeof(ipair_node_t));
        l = l->next;
        l->node.l = min;
        l->node.h = high;
        l->next = NULL;
    } else {
        /* handle continuous ranges normally */
        l->node.l = low;
        l->node.h = high;
        l->next = NULL;
    }
}

static void setgroupv(struct ParseCfgState *ncs)
{
    if (nk_gidbyname(ncs->v_str, &ncs->ce->group))
        suicide("%s: nonexistent group specified at line %zu", ncs->linenum);
}

static void setuserv(struct ParseCfgState *ncs)
{
    if (nk_uidgidbyname(ncs->v_str, &ncs->ce->user, &ncs->ce->group))
        suicide("%s: nonexistent user specified at line %zu", ncs->linenum);
}

static void parse_assign_str(char **dest, char *src, size_t srclen,
                             size_t linenum)
{
    if (srclen > INT_MAX)
        suicide("%s: srclen would overflow int at line %zu",
                __func__, linenum);
    free(*dest);
    *dest = xmalloc(srclen+1);
    ssize_t l = snprintf(*dest, srclen+1, "%.*s", (int)srclen, src);
    if (l < 0 || (size_t)l >= srclen+1)
        suicide("%s: snprintf failed l=%u srclen+1=%u at line %zu",
                __func__, l, srclen+1, linenum);
}

struct pckm {
    char *st;
    int cs;
};

%%{
    machine parse_cmd_key_m;
    access pckm.;

    action St { pckm.st = p; }
    action CmdEn {
        size_t cmdlen = p - pckm.st;
        parse_assign_str(&ncs->ce->command, pckm.st, cmdlen, ncs->linenum);
        // Unescape "\\" and "\ ".
        int prevsl = 0;
        size_t i = 0;
        while (ncs->ce->command[i]) {
            if (prevsl && (ncs->ce->command[i] == '\\'
                           || ncs->ce->command[i] == ' ')) {
                memmove(ncs->ce->command + i - 1,
                        ncs->ce->command + i, cmdlen--);
                continue;
            }
            if (ncs->ce->command[i] == '\\')
                prevsl = 1;
            ++i;
        }
    }
    action ArgEn {
        parse_assign_str(&ncs->ce->args, pckm.st, p - pckm.st, ncs->linenum);
    }

    sptab = [ \t];
    cmd = sptab+ ([^\0 \\] | '\\\\' | '\\ ')+ > 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(struct ParseCfgState *ncs)
{
    char *p = ncs->v_str;
    const char *pe = ncs->v_str + ncs->v_strlen;
    const char *eof = pe;

    struct pckm pckm = {0};

    if (ncs->cmdret != 0) {
        ncs->cmdret = -3;
        suicide("Duplicate 'command' value at line %zu", ncs->linenum);
    }

    %% write init;
    %% write exec;

    if (pckm.cs == parse_cmd_key_m_error) {
        ncs->cmdret = -1;
        suicide("Malformed 'command' value at line %zu", ncs->linenum);
    } else if (pckm.cs >= parse_cmd_key_m_first_final)
        ncs->cmdret = 1;
    else {
        ncs->cmdret = -2;
        suicide("Incomplete 'command' value at line %zu", ncs->linenum);
    }
}

static void create_ce(struct ParseCfgState *ncs)
{
    assert(!ncs->ce);
    ncs->ce = xmalloc(sizeof(cronentry_t));
    nullify_item(ncs->ce);
    ncs->cmdret = 0;
    ncs->noextime = 0;
}

#ifdef CONFIG_RL_DEBUG
static void debug_print_ce(struct ParseCfgState *ncs)
{
    printf("\nfinish_ce:\n");
    printf("id: %u\n", ncs->ce->id);
    printf("command: %s\n", ncs->ce->command);
    printf("args: %s\n", ncs->ce->args);
    printf("chroot: %s\n", ncs->ce->chroot);
    printf("numruns: %u\n", ncs->ce->numruns);
    printf("maxruns: %u\n", ncs->ce->maxruns);
    printf("journal: %d\n", ncs->ce->journal);
    printf("user: %u\n", ncs->ce->user);
    printf("group: %u\n", ncs->ce->group);
    if (ncs->ce->month)
        printf("month: [%d,%d]\n", ncs->ce->month->node.l, ncs->ce->month->node.h);
    if (ncs->ce->day)
        printf("day: [%d,%d]\n", ncs->ce->day->node.l, ncs->ce->day->node.h);
    if (ncs->ce->weekday)
        printf("weekday: [%d,%d]\n", ncs->ce->weekday->node.l, ncs->ce->weekday->node.h);
    if (ncs->ce->hour)
        printf("hour: [%d,%d]\n", ncs->ce->hour->node.l, ncs->ce->hour->node.h);
    if (ncs->ce->minute)
        printf("minute: [%d,%d]\n", ncs->ce->minute->node.l, ncs->ce->minute->node.h);
    printf("interval: %u\n", ncs->ce->interval);
    printf("exectime: %u\n", ncs->ce->exectime);
    printf("lasttime: %u\n", ncs->ce->lasttime);
}
static void debug_print_ce_ignore(struct ParseCfgState *ncs)
{
    (void)ncs;
    printf("===> IGNORE\n");
}
static void debug_print_ce_add(struct ParseCfgState *ncs)
{
    (void)ncs;
    printf("===> ADD\n");
}
#else
static void debug_print_ce(struct ParseCfgState *ncs) {(void)ncs;}
static void debug_print_ce_ignore(struct ParseCfgState *ncs) {(void)ncs;}
static void debug_print_ce_add(struct ParseCfgState *ncs) {(void)ncs;}
#endif

static void finish_ce(struct ParseCfgState *ncs)
{
    if (!ncs->ce)
        return;

    debug_print_ce(ncs);

    if (ncs->ce->id <= 0
        || (ncs->ce->interval <= 0 && ncs->ce->exectime <= 0)
        || !ncs->ce->command || ncs->cmdret < 1) {
        debug_print_ce_ignore(ncs);
        free_cronentry(&ncs->ce);
        ncs->ce = NULL;
        return;
    }
    debug_print_ce_add(ncs);

    /* we have a job to insert */
    if (ncs->ce->exectime != 0) { /* runat task */
        get_history(ncs->ce, ncs->execfile, 1);

        /* insert iif we haven't exceeded maxruns */
        if (ncs->ce->maxruns == 0 || ncs->ce->numruns < ncs->ce->maxruns)
            stack_insert(ncs->ce, &ncs->stack);
        else
            stack_insert(ncs->ce, &ncs->deadstack);
    } else { /* interval task */
        get_history(ncs->ce, ncs->execfile, ncs->noextime && !cfg_reload);

        /* compensate for user edits to job constraints */
        time_t ttm = get_first_time(ncs->ce);
        if (ttm - ncs->ce->lasttime >= ncs->ce->interval)
            ncs->ce->exectime = ttm;
        else
            force_to_constraint(ncs->ce);

        /* insert iif numruns < maxruns and no constr error */
        if ((ncs->ce->maxruns == 0 || ncs->ce->numruns < ncs->ce->maxruns)
            && ncs->ce->exectime != 0)
            stack_insert(ncs->ce, &ncs->stack);
        else
            stack_insert(ncs->ce, &ncs->deadstack);
    }
    ncs->ce = NULL;
}

%%{
    machine ncrontab;
    access ncs->;

    spc = [ \t];
    eqsep = spc+ '=' spc+;
    cmdterm = [\0\n];

    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)
                suicide("error parsing line %u in crontab; too long?",
                        ncs->linenum);
        }
    }

    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) cmdterm;
    timeval = t_any (spc* t_any)* cmdterm;
    intrangeval = (digit+ > IntValSt % IntValEn)
                  (',' (digit+ > IntVal2St % IntVal2En))? cmdterm;
    stringval = ([^\0\n]+ > StrValSt % StrValEn) cmdterm;

    action JournalEn { ncs->ce->journal = 1; }
    action NoExecTimeEn { ncs->noextime = 1; }

    journal = 'journal'i cmdterm % JournalEn;
    noexectime = 'noexectime'i cmdterm % NoExecTimeEn;

    action RunAtEn {
        ncs->ce->exectime = ncs->v_int;
        ncs->ce->maxruns = 1;
        ncs->ce->journal = 1;
    }
    action MaxRunsEn {
        if (ncs->ce->exectime == 0)
            ncs->ce->maxruns = ncs->v_int;
    }

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

    action LimAsEn { setlim(ncs, RLIMIT_AS); }
    action LimMemlockEn { setlim(ncs, RLIMIT_MEMLOCK); }
    action LimNofileEn { setlim(ncs, RLIMIT_NOFILE); }
    action LimNprocEn { setlim(ncs, RLIMIT_NPROC); }
    action LimRssEn { setlim(ncs, RLIMIT_RSS); }
    action LimCoreEn { setlim(ncs, RLIMIT_CORE); }
    action LimStackEn { setlim(ncs, RLIMIT_STACK); }
    action LimDataEn { setlim(ncs, RLIMIT_DATA); }
    action LimFsizeEn { setlim(ncs, RLIMIT_FSIZE); }
    action LimCpuEn { setlim(ncs, RLIMIT_CPU); }

    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;

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

    interval = 'interval'i eqsep timeval % IntervalEn;

    action MonthEn { addipairlist(ncs, &ncs->ce->month, 0, 1, 12); }
    action DayEn { addipairlist(ncs, &ncs->ce->day, 0, 1, 31); }
    action WeekdayEn { addipairlist(ncs, &ncs->ce->weekday, 0, 1, 7); }
    action HourEn { addipairlist(ncs, &ncs->ce->hour, 24, 0, 23); }
    action MinuteEn { addipairlist(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 { setgroupv(ncs); }
    action UserEn { setuserv(ncs); }
    action ChrootEn {
        parse_assign_str(&ncs->ce->chroot, ncs->v_str, ncs->v_strlen,
                         ncs->linenum);
    }
    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 |
           maxruns | runat | noexectime | journal;

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

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

    main := jobid | cmds;
}%%

%% write data;

static int do_parse_config(struct ParseCfgState *ncs, char *data, size_t len)
{
    char *p = data;
    const char *pe = data + len;
    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(char *path, char *execfile, cronentry_t **stk,
                  cronentry_t **deadstk)
{
    char buf[MAXLINE];
    struct ParseCfgState ncs;
    memset(&ncs, 0, sizeof ncs);
    ncs.execfile = execfile;
    ncs.stack = *stk;
    ncs.deadstack = *deadstk;

    FILE *f = fopen(path, "r");
    if (!f)
        suicide("%s: fopen(%s) failed: %s", __func__, path, strerror(errno));

    while (++ncs.linenum, fgets(buf, sizeof buf, f)) {
        int r = do_parse_config(&ncs, buf, strlen(buf));
        if (r < 0)
            suicide("%s: do_parse_config(%s) failed", __func__, path);
        if (r == 1)
            break;
    }
    if (fclose(f))
        suicide("%s: fclose(%s) failed: %s", __func__, path, strerror(errno));

    *stk = ncs.stack;
    *deadstk = ncs.deadstack;
    if (ncs.ce)
        free_cronentry(&ncs.ce); // Free partially built unused item.
    cfg_reload = 1;
}

/* config.rl - configure file parser for ncron
 *
 * (c) 2003-2014 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 <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.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>
#include "nk/log.h"
#include "nk/malloc.h"
#include "nk/privilege.h"

#include "ncron.h"
#include "config.h"
#include "sched.h"

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

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

struct ParseCfgState
{
    char v_str[1024];

    cronentry_t *stack;
    cronentry_t *deadstack;
    cronentry_t *ce;

    char *execfile;

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

    size_t v_strlen;
    size_t linenum;

    unsigned int v_jobid;
    unsigned int v_time;

    int v_int;
    int v_int2;

    int cs;
    int noextime;
    int cmdret;

    bool intv2_exist;
};

static void nullify_item(cronentry_t *item)
{
    if (!item)
        return;
    item->id = 0;
    item->command = NULL;
    item->args = NULL;
    item->chroot = NULL;
    item->numruns = 0;
    item->maxruns = 0;
    item->journal = 0;
    item->user = 0;
    item->group = 0;
    item->month = NULL;
    item->day = NULL;
    item->weekday = NULL;
    item->hour = NULL;
    item->minute = NULL;
    item->interval = 0;
    item->exectime = 0;
    item->lasttime = 0;
    item->limits = NULL;
    item->next = NULL;
}

static void nullify_limits(limit_t *l)
{
    if (!l)
        return;
    l->cpu = NULL;
    l->fsize = NULL;
    l->data = NULL;
    l->stack = NULL;
    l->core = NULL;
    l->rss = NULL;
    l->nproc = NULL;
    l->nofile = NULL;
    l->memlock = NULL;
    l->as = NULL;
}

struct hstm {
    char *st;
    int cs;
    int id;
    time_t exectime;
    time_t lasttime;
    unsigned int numruns;
    bool got_exectime:1;
    bool got_lasttime:1;
    bool got_numruns:1;
};

%%{
    machine history_m;
    access hstm->;

    action St { hstm->st = p; }
    action LastTimeEn {
        hstm->lasttime = atoi(p);
        hstm->got_lasttime = 1;
    }
    action NumRunsEn {
        hstm->numruns = atoi(p);
        hstm->got_numruns = 1;
    }
    action ExecTimeEn {
        hstm->exectime = atoi(p);
        hstm->got_exectime = 1;
    }
    action IdEn { hstm->id = atoi(p); }

    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_get_history(struct hstm *hstm, char *buf, size_t blen)
{
    char *p = buf;
    const char *pe = buf + blen;
    const char *eof = pe;

    %% write init;
    %% write exec;

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

static void get_history(cronentry_t *item, char *path, int noextime)
{
    struct hstm hstm = {0};

    assert(item);

    if (!noextime && !hstm.got_exectime)
        item->exectime = 0;

    FILE *f = fopen(path, "r");
    if (!f) {
        log_line("failed to open history file \"%s\" for read", path);
        if (!noextime)
            item->exectime = (time_t)0; /* gracefully fail */
        return;
    }

    char buf[MAXLINE];
    while (fgets(buf, sizeof buf, f)) {
        int r = do_get_history(&hstm, buf, strlen(buf));
        if (r < 0)
            suicide("%s: do_get_history(%s) failed", __func__, path);
        if (r == 1) {
            if (hstm.id == item->id) {
                if (hstm.got_lasttime)
                    item->lasttime = hstm.lasttime;
                if (hstm.got_numruns)
                    item->numruns = hstm.numruns;
                if (hstm.got_exectime && !noextime)
                    item->exectime = hstm.exectime;
                break;
            }
        }
    }
    if (fclose(f))
        suicide("%s: fclose(%s) failed: %s", __func__, path, strerror(errno));
}

static void setlim(struct ParseCfgState *ncs, int type)
{
    struct rlimit **p = NULL;
    if (!ncs->ce->limits) {
        ncs->ce->limits = xmalloc(sizeof(limit_t));
        nullify_limits(ncs->ce->limits);
    }
    switch (type) {
    case RLIMIT_CPU: *p = ncs->ce->limits->cpu; break;
    case RLIMIT_FSIZE: *p = ncs->ce->limits->fsize; break;
    case RLIMIT_DATA: *p = ncs->ce->limits->data; break;
    case RLIMIT_STACK: *p = ncs->ce->limits->stack; break;
    case RLIMIT_CORE: *p = ncs->ce->limits->core; break;
    case RLIMIT_RSS: *p = ncs->ce->limits->rss; break;
    case RLIMIT_NPROC: *p = ncs->ce->limits->nproc; break;
    case RLIMIT_NOFILE: *p = ncs->ce->limits->nofile; break;
    case RLIMIT_MEMLOCK: *p = ncs->ce->limits->memlock; break;
#ifndef BSD
    case RLIMIT_AS: *p = ncs->ce->limits->as; break;
#endif /* BSD */
    default: return;
    }
    if (!p)
        suicide("%s: unexpected NULL, corruption?", __func__);
    if (!*p)
        *p = xmalloc(sizeof(struct rlimit));
    if (ncs->v_int == 0) (*p)->rlim_cur = RLIM_INFINITY;
    else (*p)->rlim_cur = ncs->v_int;

    if (ncs->v_int2 == 0) (*p)->rlim_max = RLIM_INFINITY;
    else (*p)->rlim_cur = ncs->v_int2;
}

static void addipairlist(struct ParseCfgState *ncs,
                         ipair_node_t **list, int wildcard, int min, int max)
{
    ipair_node_t *l;
    int low, high = wildcard;

    low = ncs->v_int;
    if (ncs->intv2_exist)
        high = ncs->v_int2;

    if (high == wildcard)
        high = low;
    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 (*list == NULL) {
        *list = xmalloc(sizeof(ipair_node_t));
        l = *list;
    } else {
        l = *list;
        while (l->next)
            l = l->next;

        l->next = xmalloc(sizeof(ipair_node_t));
        l = l->next;
    }

    /* discontinuous range, split into two continuous rules... */
    if (low > high) {
        l->node.l = low;
        l->node.h = max;
        l->next = xmalloc(sizeof(ipair_node_t));
        l = l->next;
        l->node.l = min;
        l->node.h = high;
        l->next = NULL;
    } else {
        /* handle continuous ranges normally */
        l->node.l = low;
        l->node.h = high;
        l->next = NULL;
    }
}

static void setgroupv(struct ParseCfgState *ncs)
{
    if (nk_gidbyname(ncs->v_str, &ncs->ce->group))
        suicide("%s: nonexistent group specified at line %zu", ncs->linenum);
}

static void setuserv(struct ParseCfgState *ncs)
{
    if (nk_uidgidbyname(ncs->v_str, &ncs->ce->user, &ncs->ce->group))
        suicide("%s: nonexistent user specified at line %zu", ncs->linenum);
}

static void parse_assign_str(char **dest, char *src, size_t srclen,
                             size_t linenum)
{
    if (srclen > INT_MAX)
        suicide("%s: srclen would overflow int at line %zu",
                __func__, linenum);
    free(*dest);
    *dest = xmalloc(srclen+1);
    ssize_t l = snprintf(*dest, srclen+1, "%.*s", (int)srclen, src);
    if (l < 0 || (size_t)l >= srclen+1)
        suicide("%s: snprintf failed l=%u srclen+1=%u at line %zu",
                __func__, l, srclen+1, linenum);
}

struct pckm {
    char *st;
    int cs;
};

%%{
    machine parse_cmd_key_m;
    access pckm.;

    action St { pckm.st = p; }
    action CmdEn {
        size_t cmdlen = p - pckm.st;
        parse_assign_str(&ncs->ce->command, pckm.st, cmdlen, ncs->linenum);
        // Unescape "\\" and "\ ".
        int prevsl = 0;
        size_t i = 0;
        while (ncs->ce->command[i]) {
            if (prevsl && (ncs->ce->command[i] == '\\'
                           || ncs->ce->command[i] == ' ')) {
                memmove(ncs->ce->command + i - 1,
                        ncs->ce->command + i, cmdlen--);
                continue;
            }
            if (ncs->ce->command[i] == '\\')
                prevsl = 1;
            ++i;
        }
    }
    action ArgEn {
        parse_assign_str(&ncs->ce->args, pckm.st, p - pckm.st, ncs->linenum);
    }

    sptab = [ \t];
    cmd = sptab+ ([^\0 \\] | '\\\\' | '\\ ')+ > 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(struct ParseCfgState *ncs)
{
    char *p = ncs->v_str;
    const char *pe = ncs->v_str + ncs->v_strlen;
    const char *eof = pe;

    struct pckm pckm = {0};

    if (ncs->cmdret != 0) {
        ncs->cmdret = -3;
        suicide("Duplicate 'command' value at line %zu", ncs->linenum);
    }

    %% write init;
    %% write exec;

    if (pckm.cs == parse_cmd_key_m_error) {
        ncs->cmdret = -1;
        suicide("Malformed 'command' value at line %zu", ncs->linenum);
    } else if (pckm.cs >= parse_cmd_key_m_first_final)
        ncs->cmdret = 1;
    else {
        ncs->cmdret = -2;
        suicide("Incomplete 'command' value at line %zu", ncs->linenum);
    }
}

static void create_ce(struct ParseCfgState *ncs)
{
    assert(!ncs->ce);
    ncs->ce = xmalloc(sizeof(cronentry_t));
    nullify_item(ncs->ce);
    ncs->cmdret = 0;
    ncs->noextime = 0;
}

static void finish_ce(struct ParseCfgState *ncs)
{
    assert(ncs->ce);
    if (ncs->ce->id <= 0
        || (ncs->ce->interval <= 0 && ncs->ce->exectime <= 0)
        || !ncs->ce->command || ncs->cmdret < 1) {
        free_cronentry(&ncs->ce);
        return;
    }

    /* we have a job to insert */
    if (ncs->ce->exectime != 0) { /* runat task */
        get_history(ncs->ce, ncs->execfile, 1);

        /* insert iif we haven't exceeded maxruns */
        if (ncs->ce->maxruns == 0 || ncs->ce->numruns < ncs->ce->maxruns)
            stack_insert(ncs->ce, &ncs->stack);
        else
            stack_insert(ncs->ce, &ncs->deadstack);
    } else { /* interval task */
        get_history(ncs->ce, ncs->execfile, ncs->noextime && !cfg_reload);

        /* compensate for user edits to job constraints */
        time_t ttm = get_first_time(ncs->ce);
        if (ttm - ncs->ce->lasttime >= ncs->ce->interval)
            ncs->ce->exectime = ttm;
        else
            force_to_constraint(ncs->ce);

        /* insert iif numruns < maxruns and no constr error */
        if ((ncs->ce->maxruns == 0 || ncs->ce->numruns < ncs->ce->maxruns)
            && ncs->ce->exectime != 0)
            stack_insert(ncs->ce, &ncs->stack);
        else
            stack_insert(ncs->ce, &ncs->deadstack);
    }
    ncs->ce = NULL;
}

%%{
    machine ncrontab;
    access ncs->;

    spc = [ \t];
    eqsep = spc+ '=' spc+;
    cmdterm = [\0\n];

    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)
                suicide("error parsing line %u in crontab; too long?",
                        ncs->linenum);
        }
    }

    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) cmdterm;
    timeval = t_any (spc* t_any)* cmdterm;
    intrangeval = (digit+ > IntValSt % IntValEn)
                  (',' (digit+ > IntVal2St % IntVal2En))? cmdterm;
    stringval = ([^\0\n]+ > StrValSt % StrValEn) cmdterm;

    action JournalEn { ncs->ce->journal = 1; }
    action NoExecTimeEn { ncs->noextime = 1; }

    journal = 'journal'i cmdterm % JournalEn;
    noexectime = 'noexectime'i cmdterm % NoExecTimeEn;

    action RunAtEn {
        ncs->ce->exectime = ncs->v_int;
        ncs->ce->maxruns = 1;
        ncs->ce->journal = 1;
    }
    action MaxRunsEn {
        if (ncs->ce->exectime == 0)
            ncs->ce->maxruns = ncs->v_int;
    }

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

    action LimAsEn { setlim(ncs, RLIMIT_AS); }
    action LimMemlockEn { setlim(ncs, RLIMIT_MEMLOCK); }
    action LimNofileEn { setlim(ncs, RLIMIT_NOFILE); }
    action LimNprocEn { setlim(ncs, RLIMIT_NPROC); }
    action LimRssEn { setlim(ncs, RLIMIT_RSS); }
    action LimCoreEn { setlim(ncs, RLIMIT_CORE); }
    action LimStackEn { setlim(ncs, RLIMIT_STACK); }
    action LimDataEn { setlim(ncs, RLIMIT_DATA); }
    action LimFsizeEn { setlim(ncs, RLIMIT_FSIZE); }
    action LimCpuEn { setlim(ncs, RLIMIT_CPU); }

    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;

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

    interval = 'interval'i eqsep timeval % IntervalEn;

    action MonthEn { addipairlist(ncs, &ncs->ce->month, 0, 1, 12); }
    action DayEn { addipairlist(ncs, &ncs->ce->day, 0, 1, 31); }
    action WeekdayEn { addipairlist(ncs, &ncs->ce->weekday, 0, 1, 7); }
    action HourEn { addipairlist(ncs, &ncs->ce->hour, 24, 0, 23); }
    action MinuteEn { addipairlist(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 { setgroupv(ncs); }
    action UserEn { setuserv(ncs); }
    action ChrootEn {
        parse_assign_str(&ncs->ce->chroot, ncs->v_str, ncs->v_strlen,
                         ncs->linenum);
    }
    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 |
           maxruns | runat | noexectime | journal;

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

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

    main := jobid | cmds;
}%%

%% write data;

static int do_parse_config(struct ParseCfgState *ncs, char *data, size_t len)
{
    char *p = data;
    const char *pe = data + len;
    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(char *path, char *execfile, cronentry_t **stk,
                  cronentry_t **deadstk)
{
    char buf[MAXLINE];
    struct ParseCfgState ncs;
    memset(&ncs, 0, sizeof ncs);
    ncs.execfile = execfile;
    ncs.stack = *stk;
    ncs.deadstack = *deadstk;

    FILE *f = fopen(path, "r");
    if (!f)
        suicide("%s: fopen(%s) failed: %s", __func__, path, strerror(errno));

    while (++ncs.linenum, fgets(buf, sizeof buf, f)) {
        int r = do_parse_config(&ncs, buf, strlen(buf));
        if (r < 0)
            suicide("%s: do_parse_config(%s) failed", __func__, path);
        if (r == 1)
            break;
    }
    if (fclose(f))
        suicide("%s: fclose(%s) failed: %s", __func__, path, strerror(errno));

    *stk = ncs.stack;
    *deadstk = ncs.deadstack;
    if (ncs.ce)
        free_cronentry(&ncs.ce); // Free partially built unused item.
    cfg_reload = 1;
}