#include "php_snuffleupagus.h"

#define SP_SET_ENABLE_DISABLE(enable, disable, varname) \
  if (enable && disable) { \
    sp_log_err("config", "A rule can't be enabled and disabled on line %zu", parsed_rule->lineno); \
    return SP_PARSER_ERROR; \
  } \
  if (enable || disable) { \
    (varname) = (enable || !disable); \
  }

#define SP_PROCESS_CONFIG_KEYWORDS(CMD) if (sp_process_rule(&(parsed_rule[1]), config_keywords) != SUCCESS) { CMD; }
#define SP_PROCESS_CONFIG_KEYWORDS_ERR() SP_PROCESS_CONFIG_KEYWORDS(return SP_PARSER_ERROR)

SP_PARSE_FN(parse_enable) {
  bool enable = false, disable = false;
  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  SP_SET_ENABLE_DISABLE(enable, disable, *(bool*)retval);

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_session) {
  sp_config_session *cfg = retval;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENCRYPT, &(cfg->encrypt)},
      {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)},
      {parse_uint, SP_TOKEN_SID_MIN_LENGTH, &(cfg->sid_min_length)},
      {parse_uint, SP_TOKEN_SID_MAX_LENGTH, &(cfg->sid_max_length)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  if (cfg->encrypt) {
    if (!SPCFG(cookies_env_var)) {
      sp_log_err("config", "You're trying to use the session cookie encryption feature "
          "on line %zu without having set the `.cookie_env_var` option in "
          "`sp.global`: please set it first", parsed_rule->lineno);
      return SP_PARSER_ERROR;
    } else if (!SPCFG(encryption_key)) {
      sp_log_err("config", "You're trying to use the session cookie encryption feature "
                 "on line %zu without having set the `.secret_key` option in "
                 "`sp.global`: please set it first", parsed_rule->lineno);
      return SP_PARSER_ERROR;
    }
  }

  return SP_PARSER_STOP;
}

SP_PARSEKW_FN(parse_log_media) {
  sp_config_log *cfg = (sp_config_log*)retval;
  cfg->path = NULL;

  SP_PARSE_ARG(value);

  if (!strcmp(ZSTR_VAL(value), "php")) {
    cfg->type = SP_LOG_ZEND;
    zend_string_release_ex(value, 1);
    return SP_PARSER_STOP;
  } else if (!strcmp(ZSTR_VAL(value), "syslog")) {
    cfg->type = SP_LOG_SYSLOG;
    zend_string_release_ex(value, 1);
    return SP_PARSER_STOP;
  } else if (!strncmp(ZSTR_VAL(value), "file:", strlen("file:"))) {
    cfg->type = SP_LOG_FILE;
    cfg->path = strdup(ZSTR_VAL(value)+strlen("file:"));
    zend_string_release_ex(value, 1);
    return SP_PARSER_STOP;
  }

  sp_log_err("config", "." SP_TOKEN_LOG_MEDIA "() only supports 'syslog', 'file:' or 'php' on line %zu, got '%s' instead", kw->lineno, ZSTR_VAL(value));

  return SP_PARSER_ERROR;
}

SP_PARSE_FN(parse_unserialize_noclass) {
  bool enable = false, disable = false;
  sp_config_unserialize_noclass *cfg = (sp_config_unserialize_noclass*)retval;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);

  cfg->textual_representation = sp_get_textual_representation(parsed_rule);

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_unserialize) {
  bool enable = false, disable = false;
  sp_config_unserialize *cfg = (sp_config_unserialize*)retval;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)},
      {parse_str, SP_TOKEN_DUMP, &(cfg->dump)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);

  cfg->textual_representation = sp_get_textual_representation(parsed_rule);

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_readonly_exec) {
  bool enable = false, disable = false, xchecks = false, no_xchecks = false;
  sp_config_readonly_exec *cfg = (sp_config_readonly_exec*)retval;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)},
      {parse_str, SP_TOKEN_DUMP, &(cfg->dump)},
      {parse_empty, "extended_checks", &(xchecks)},
      {parse_empty, "xchecks", &(xchecks)},
      {parse_empty, "no_extended_checks", &(no_xchecks)},
      {parse_empty, "noxchecks", &(no_xchecks)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  cfg->textual_representation = sp_get_textual_representation(parsed_rule);

  SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);
  if (xchecks) { cfg->extended_checks = true; } else if (no_xchecks) { cfg->extended_checks = false; }

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_global) {
  sp_config_keyword config_keywords[] = {
      {parse_str, SP_TOKEN_ENCRYPTION_KEY, &(SPCFG(encryption_key))},
      {parse_str, SP_TOKEN_ENV_VAR, &(SPCFG(cookies_env_var))},
      {parse_log_media, SP_TOKEN_LOG_MEDIA, &(SPCFG(log_media))},
      {parse_uint, SP_TOKEN_LOG_MAX_LEN, &(SPCFG(log_max_len))},
      {parse_uint, SP_TOKEN_MAX_EXECUTION_DEPTH, &(SPCFG(max_execution_depth))},
      {parse_enable, SP_TOKEN_SERVER_ENCODE, &(SPCFG(server_encode))},
      {parse_enable, SP_TOKEN_SERVER_STRIP, &(SPCFG(server_strip))},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  if (SPCFG(encryption_key)) {
    if (ZSTR_LEN(SPCFG(encryption_key)) < 10) {
      sp_log_err("config", "The encryption key set on line %zu is too short. please use at least 10 bytes", parsed_rule->lineno);
      return SP_PARSER_ERROR;
    }
    if (zend_string_equals_literal(SPCFG(encryption_key), "YOU _DO_ NEED TO CHANGE THIS WITH SOME RANDOM CHARACTERS.") ||
        zend_string_equals_literal(SPCFG(encryption_key), "c6a0e02b3b818f7559d5f85303d8fe44")) {
      sp_log_err("config", "The encryption key set on line %zu is an unchanged dummy value. please use a unique secret.", parsed_rule->lineno);
      return SP_PARSER_ERROR;
    }
  }

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_eval_filter_conf) {
  sp_config_eval *cfg = &(SPCFG(eval));

  sp_config_keyword config_keywords[] = {
      {parse_list, SP_TOKEN_LIST, retval},
      {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)},
      {parse_str, SP_TOKEN_DUMP, &(cfg->dump)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  cfg->textual_representation = sp_get_textual_representation(parsed_rule);

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_wrapper_whitelist) {
  sp_config_wrapper *cfg = (sp_config_wrapper*)retval;
  
  sp_config_keyword config_keywords[] = {
      {parse_list, SP_TOKEN_LIST, &cfg->whitelist},
      {parse_list, SP_TOKEN_ALLOW_PHP_STREAMS, &cfg->php_stream_allowlist},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  cfg->enabled = true;

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_cookie) {
  zend_string *samesite = NULL;
  sp_cookie *cookie = pecalloc(1, sizeof(sp_cookie), 1);

  sp_config_keyword config_keywords[] = {
      {parse_str, SP_TOKEN_NAME, &(cookie->name)},
      {parse_regexp, SP_TOKEN_NAME_REGEXP, &(cookie->name_r)},
      {parse_str, SP_TOKEN_SAMESITE, &samesite},
      {parse_empty, SP_TOKEN_ENCRYPT, &cookie->encrypt},
      {parse_empty, SP_TOKEN_SIMULATION, &cookie->simulation},
      {parse_empty, SP_TOKEN_SIM, &cookie->simulation},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS(goto err);

  if (cookie->encrypt) {
    if (!SPCFG(cookies_env_var)) {
      sp_log_err("config", "You're trying to use the cookie encryption feature on line %zu "
                            "without having set the `." SP_TOKEN_ENV_VAR "` option in `sp.global`: please set it first", parsed_rule->lineno);
      goto err;
    } else if (!SPCFG(encryption_key)) {
      sp_log_err("config", "You're trying to use the cookie encryption feature "
          "on line %zu without having set the `." SP_TOKEN_ENCRYPTION_KEY "` option in "
          "`sp." SP_TOKEN_GLOBAL "`: please set it first", parsed_rule->lineno);
      goto err;
    }
  } else if (!samesite) {
    sp_log_err("config", "You must specify a at least one action to a cookie on line %zu", parsed_rule->lineno);
    goto err;
  }
  if ((!cookie->name || 0 == ZSTR_LEN(cookie->name)) && !cookie->name_r) {
    sp_log_err("config", "You must specify a cookie name/regexp on line %zu", parsed_rule->lineno);
    goto err;
  }
  if (cookie->name && cookie->name_r) {
    sp_log_err("config", "name and name_r are mutually exclusive on line %zu", parsed_rule->lineno);
    goto err;
  }
  if (samesite) {
    if (zend_string_equals_literal_ci(samesite, SP_TOKEN_SAMESITE_LAX)) {
      cookie->samesite = lax;
    } else if (zend_string_equals_literal_ci(samesite, SP_TOKEN_SAMESITE_STRICT)) {
      cookie->samesite = strict;
    } else {
      sp_log_err("config", "'%s' is an invalid value to samesite (expected " SP_TOKEN_SAMESITE_LAX " or " SP_TOKEN_SAMESITE_STRICT ") on line %zu",
        ZSTR_VAL(samesite), parsed_rule->lineno);
      goto err;
    }
    zend_string_release(samesite);
    samesite = NULL;
  }

  // find other cookie entry with identical name or name_r
  sp_cookie *entry = NULL;
  sp_list_node *pList = NULL;
  for (pList = SPCFG(cookie).cookies; pList; pList = pList->next) {
    entry = pList->data;
    if (!entry) { continue; }
    if ((entry->name && cookie->name && sp_zend_string_equals(entry->name, cookie->name)) ||
        (entry->name_r && cookie->name_r && sp_zend_string_equals(entry->name_r->pattern, cookie->name_r->pattern))) {
          break;
        }
  }
  if (pList && entry) {
    // override cookie settings if set
    if (cookie->samesite) { entry->samesite = cookie->samesite; }
    if (cookie->encrypt) { entry->encrypt = cookie->encrypt; }
    if (cookie->simulation) { entry->simulation = cookie->simulation; }
    sp_free_cookie(cookie);
    pefree(cookie, 1);
    cookie = NULL;
  } else {
    SPCFG(cookie).cookies = sp_list_insert(SPCFG(cookie).cookies, cookie);
  }

  return SP_PARSER_STOP;

err:
  if (samesite) {
    zend_string_release(samesite);
  }
  if (cookie) {
    sp_free_cookie(cookie);
    pefree(cookie, 1);
  }
  return SP_PARSER_ERROR;
}

static int add_df_to_hashtable(HashTable *ht, sp_disabled_function *df) {
  zval *list = zend_hash_find(ht, df->function);

  if (NULL == list) {
    zend_hash_add_ptr(ht, df->function, sp_list_insert(NULL, df));
  } else {
    Z_PTR_P(list) = sp_list_insert(Z_PTR_P(list), df);
  }
  return SUCCESS;
}

SP_PARSE_FN(parse_disabled_functions) {
  int ret = SP_PARSER_ERROR;
  bool enable = false, disable = false, allow = false, drop = false;
  zend_string *var = NULL, *param = NULL;
  sp_disabled_function *df = pecalloc(1, sizeof(*df), 1);
  df->pos = -1;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {parse_str, SP_TOKEN_ALIAS, &(df->alias)},
      {parse_empty, SP_TOKEN_SIMULATION, &(df->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(df->simulation)},
      {parse_str, SP_TOKEN_FILENAME, &(df->filename)},
      {parse_regexp, SP_TOKEN_FILENAME_REGEXP, &(df->r_filename)},
      {parse_str, SP_TOKEN_FUNCTION, &(df->function)},
      {parse_regexp, SP_TOKEN_FUNCTION_REGEXP, &(df->r_function)},
      {parse_str, SP_TOKEN_DUMP, &(df->dump)},
      {parse_empty, SP_TOKEN_ALLOW, &(allow)},
      {parse_empty, SP_TOKEN_DROP, &(drop)},
      {parse_str, SP_TOKEN_HASH, &(df->hash)},
      {parse_str, SP_TOKEN_PARAM, &(param)},
      {parse_regexp, SP_TOKEN_VALUE_REGEXP, &(df->r_value)},
      {parse_str, SP_TOKEN_VALUE, &(df->value)},
      {parse_str, SP_TOKEN_KEY, &(df->key)},
      {parse_regexp, SP_TOKEN_KEY_REGEXP, &(df->r_key)},
      {parse_regexp, SP_TOKEN_PARAM_REGEXP, &(df->r_param)},
      {parse_php_type, SP_TOKEN_PARAM_TYPE, &(df->param_type)},
      {parse_str, SP_TOKEN_RET, &(df->ret)},
      {parse_cidr, SP_TOKEN_CIDR, &(df->cidr)},
      {parse_regexp, SP_TOKEN_RET_REGEXP, &(df->r_ret)},
      {parse_php_type, SP_TOKEN_RET_TYPE, &(df->ret_type)},
      {parse_str, SP_TOKEN_LOCAL_VAR, &(var)},
      {parse_int, SP_TOKEN_VALUE_ARG_POS, &(df->pos)},
      {parse_uint, SP_TOKEN_LINE_NUMBER, &(df->line)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS(goto out);

  SP_SET_ENABLE_DISABLE(enable, disable, enable);
  if (disable) {
    ret = SP_PARSER_STOP; goto out;
  }

#define MUTUALLY_EXCLUSIVE(X, Y, STR1, STR2)                             \
  if (X && Y) {                                                          \
    sp_log_err("config", "Invalid configuration line for 'sp.disabled_functions': '.%s' and '.%s' are mutually exclusive on line %zu", STR1, STR2, parsed_rule->lineno); \
    goto out;                                                  \
  }

  MUTUALLY_EXCLUSIVE(df->value, df->r_value, "value", "value_r");
  MUTUALLY_EXCLUSIVE(df->r_function, df->function, "function", "function_r");
  MUTUALLY_EXCLUSIVE(df->filename, df->r_filename, "filename", "filename_r");
  MUTUALLY_EXCLUSIVE(df->ret, df->r_ret, "ret", "ret_r");
  MUTUALLY_EXCLUSIVE(df->key, df->r_key, "key", "key_r");
  MUTUALLY_EXCLUSIVE((df->pos >= 0), param, "pos", "param");
  MUTUALLY_EXCLUSIVE((df->pos >= 0), df->r_param, "pos", "param_r");
  MUTUALLY_EXCLUSIVE(param, df->r_param, "param", "param_r");
  MUTUALLY_EXCLUSIVE((df->r_key || df->key), (df->r_value || df->value), "key", "value");
  MUTUALLY_EXCLUSIVE((df->r_ret || df->ret || df->ret_type), (df->r_param || param), "ret", "param");
  MUTUALLY_EXCLUSIVE((df->r_ret || df->ret || df->ret_type), (var), "ret", "var");
  MUTUALLY_EXCLUSIVE((df->r_ret || df->ret || df->ret_type), (df->value || df->r_value), "ret", "value");

#undef MUTUALLY_EXCLUSIVE

  if (!(df->r_function || df->function)) {
    sp_log_err("config", "Invalid configuration line: 'sp.disabled_functions': must take a function name on line %zu", parsed_rule->lineno);
    goto out;
  }
  if (df->filename && (*ZSTR_VAL(df->filename) != '/') &&
             (0 != strncmp(ZSTR_VAL(df->filename), "phar://", strlen("phar://")))) {
    sp_log_err("config", "Invalid configuration line: 'sp.disabled_functions': '.filename' must be an absolute path or a phar archive on line %zu", parsed_rule->lineno);
    goto out;
  }
  if (!(allow ^ drop)) {
    sp_log_err("config", "Invalid configuration line: 'sp.disabled_functions': The rule must either be a `drop` or `allow` one on line %zu", parsed_rule->lineno);
    goto out;
  }

  df->allow = allow;
  df->textual_representation = sp_get_textual_representation(parsed_rule);

  if (df->function) {
    df->functions_list = parse_functions_list(ZSTR_VAL(df->function));
  }

  if (param) {
    if (ZSTR_LEN(param) > 0 && ZSTR_VAL(param)[0] != '$') {
      /* This is an ugly hack. We're prefixing with a `$` because otherwise
       * the parser treats this as a constant.
       * FIXME: Remove this, and improve our (weird) parser. */
      char *new = pecalloc(ZSTR_LEN(param) + 2, 1, 1);
      new[0] = '$';
      memcpy(new + 1, ZSTR_VAL(param), ZSTR_LEN(param));
      df->param = sp_parse_var(new);
      pefree(new, 1);
    } else {
      df->param = sp_parse_var(ZSTR_VAL(param));
    }
    if (!df->param) {
      sp_log_err("config", "Invalid value '%s' for `param` on line %zu", ZSTR_VAL(param), parsed_rule->lineno);
      goto out;
    }
  }
  if (var) {
    if (ZSTR_LEN(var)) {
      df->var = sp_parse_var(ZSTR_VAL(var));
      if (!df->var) {
        sp_log_err("config", "Invalid value '%s' for `var` on line %zu", ZSTR_VAL(var), parsed_rule->lineno);
        goto out;
      }
    } else {
      sp_log_err("config", "Empty value in `var` on line %zu", parsed_rule->lineno);
      goto out;
    }
  }

  if (df->function && zend_string_equals_literal(df->function, "print")) {
    zend_string_release(df->function);
    df->function = zend_string_init(ZEND_STRL("echo"), 1);
  }

  if (df->function && !df->functions_list) {
    if (df->ret || df->r_ret || df->ret_type) {
      add_df_to_hashtable(SPCFG(disabled_functions_ret), df);
    } else {
      add_df_to_hashtable(SPCFG(disabled_functions), df);
    }
  } else {
    if (df->ret || df->r_ret || df->ret_type) {
      SPCFG(disabled_functions_reg_ret).disabled_functions = sp_list_insert(SPCFG(disabled_functions_reg_ret).disabled_functions, df);
    } else {
      SPCFG(disabled_functions_reg).disabled_functions = sp_list_insert(SPCFG(disabled_functions_reg).disabled_functions, df);
    }
  }
  return SP_PARSER_STOP;

out:
  sp_free_disabled_function(df);
  pefree(df, 1);
  if (param) { zend_string_release(param); }
  if (var) { zend_string_release(var); }

  return ret;
}

SP_PARSE_FN(parse_upload_validation) {
  bool disable = false, enable = false;
  sp_config_upload_validation *cfg = (sp_config_upload_validation*)retval;

  sp_config_keyword config_keywords[] = {
      {parse_empty, SP_TOKEN_ENABLE, &(enable)},
      {parse_empty, SP_TOKEN_DISABLE, &(disable)},
      {parse_str, SP_TOKEN_UPLOAD_SCRIPT, &(cfg->script)},
      {parse_empty, SP_TOKEN_SIMULATION, &(cfg->simulation)},
      {parse_empty, SP_TOKEN_SIM, &(cfg->simulation)},
      {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();
  SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);

  if (!cfg->script) {
    sp_log_err("config", "The `script` directive is mandatory in '.%s' on line %zu", token, parsed_rule->lineno);
    return SP_PARSER_ERROR;
  } else if (-1 == access(ZSTR_VAL(cfg->script), F_OK)) {
    sp_log_err("config", "The `script` (%s) doesn't exist on line %zu", ZSTR_VAL(cfg->script), parsed_rule->lineno);
    return SP_PARSER_ERROR;
  }

  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_ini_protection) {
  bool disable = false, enable = false;
  bool rw = false, ro = false; // rw is ignored, but declaring .policy_rw is valid for readability
  sp_config_ini *cfg = (sp_config_ini*)retval;
  sp_config_keyword config_keywords[] = {
    {parse_empty, "enable", &(enable)},
    {parse_empty, "disable", &(disable)},
    {parse_empty, "simulation", &cfg->simulation},
    {parse_empty, "sim", &cfg->simulation},
    {parse_empty, "policy_readonly", &ro},
    {parse_empty, "policy_ro", &ro},
    {parse_empty, "policy_readwrite", &rw},
    {parse_empty, "policy_rw", &rw},
    {parse_empty, "policy_silent_ro", &cfg->policy_silent_ro},
    {parse_empty, "policy_silent_fail", &cfg->policy_silent_fail},
    {parse_empty, "policy_no_log", &cfg->policy_silent_fail},
    {parse_empty, "policy_drop", &cfg->policy_drop},
    {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS_ERR();

  SP_SET_ENABLE_DISABLE(enable, disable, cfg->enable);

  if (ro && rw) {
    sp_log_err("config", "rule cannot be both read-write and read-only on line %zu", parsed_rule->lineno);
    return SP_PARSER_ERROR;
  }
  cfg->policy_readonly = ro;

  if (cfg->policy_silent_fail && cfg->policy_drop) {
    sp_log_err("config", "policy cannot be drop and silent at the same time on line %zu", parsed_rule->lineno);
    return SP_PARSER_ERROR;
  }
  return SP_PARSER_STOP;
}

SP_PARSE_FN(parse_ini_entry) {
  sp_ini_entry *entry = pecalloc(1, sizeof(sp_ini_entry), 1);
  bool rw = false, ro = false;

  sp_config_keyword config_keywords[] = {
    {parse_empty, "simulation", &entry->simulation},
    {parse_empty, "sim", &entry->simulation},
    {parse_str, "key", &entry->key},
    {parse_str, "msg", &entry->msg},
    {parse_str, "set", &entry->set},
    {parse_str, "min", &entry->min},
    {parse_str, "max", &entry->max},
    {parse_regexp, "regexp", &entry->regexp},
    {parse_empty, "readonly", &ro},
    {parse_empty, "ro", &ro},
    {parse_empty, "readwrite", &rw},
    {parse_empty, "rw", &rw},
    {parse_empty, "drop", &entry->drop},
    {parse_empty, "allow_null", &entry->allow_null},
    {0, 0, 0}};

  SP_PROCESS_CONFIG_KEYWORDS(goto err);

   if (!entry->key) {
    sp_log_err("config", "A .key() must be provided on line %zu", parsed_rule->lineno);
    goto err;
  }

  if (zend_hash_find_ptr(SPCFG(ini).entries, entry->key)) {
    sp_log_err("config", "duplicate INI key '%s' on line %zu", ZSTR_VAL(entry->key), parsed_rule->lineno);
    goto err;
  }

  if (ro && rw) {
    sp_log_err("config", "rule cannot be both read-write and read-only on line %zu", parsed_rule->lineno);
    goto err;
  }
  entry->access = ro - rw;

  zend_hash_add_ptr(SPCFG(ini).entries, entry->key, entry);
  return SP_PARSER_STOP;

err:
  if (entry) {
    sp_free_ini_entry(entry);
    pefree(entry, 1);
  }
  return SP_PARSER_ERROR;
}
