/* authz_parse.c : Parser for path-based access control
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); 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.
 * ====================================================================
 */

#include <apr_fnmatch.h>
#include <apr_tables.h>

#include "svn_ctype.h"
#include "svn_error.h"
#include "svn_hash.h"
#include "svn_iter.h"
#include "svn_pools.h"
#include "svn_repos.h"

#include "private/svn_fspath.h"
#include "private/svn_config_private.h"
#include "private/svn_sorts_private.h"
#include "private/svn_string_private.h"
#include "private/svn_subr_private.h"

#include "svn_private_config.h"

#include "authz.h"


/* Temporary ACL constructed by the parser. */
typedef struct parsed_acl_t
{
  /* The global ACL.
     The strings in ACL.rule are allocated from the result pool.
     ACL.user_access is null during the parsing stage. */
  authz_acl_t acl;

  /* The set of access control entries. In the second pass, aliases in
     these entries will be expanded and equivalent entries will be
     merged. The entries are allocated from the parser pool. */
  apr_hash_t *aces;

  /* The set of access control entries that use aliases. In the second
     pass, aliases in these entries will be expanded and merged into ACES.
     The entries are allocated from the parser pool. */
  apr_hash_t *alias_aces;
} parsed_acl_t;


/* Temporary group definition constructed by the authz/group parser.
   Once all groups and aliases are defined, a second pass over these
   data will recursively expand group memberships. */
typedef struct parsed_group_t
{
  svn_boolean_t local_group;
  apr_array_header_t *members;
} parsed_group_t;


/* Baton for the parser constructor. */
typedef struct ctor_baton_t
{
  /* The final output of the parser. */
  authz_full_t *authz;

  /* Interned-string set, allocated in AUTHZ->pool.
     Stores singleton instances of user, group and repository names,
     which are used by members of the AUTHZ structure. By reusing the
     same immutable string multiple times, we reduce the size of the
     authz representation in the result pool.

     N.B.: Whilst the strings are allocated from teh result pool, the
     hash table itself is not. */
  apr_hash_t *strings;

  /* A set of all the sections that were seen in the authz or global
     groups file. Rules, aliases and groups may each only be defined
     once in the authz file. The global groups file may only contain a
     [groups] section. */
  apr_hash_t *sections;

  /* The name of the section we're currently parsing. */
  const char *section;

  /* TRUE iff we're parsing the global groups file. */
  svn_boolean_t parsing_groups;

  /* TRUE iff we're parsing a [groups] section. */
  svn_boolean_t in_groups;

  /* TRUE iff we're parsing an [aliases] section. */
  svn_boolean_t in_aliases;

  /* A set of all the unique rules we parsed from the section names. */
  apr_hash_t *parsed_rules;

  /* Temporary parsed-groups definitions. */
  apr_hash_t *parsed_groups;

  /* Temporary alias mappings. */
  apr_hash_t *parsed_aliases;

  /* Temporary parsed-acl definitions. */
  apr_array_header_t *parsed_acls;

  /* Temporary expanded groups definitions. */
  apr_hash_t *expanded_groups;

  /* The temporary ACL we're currently constructing. */
  parsed_acl_t *current_acl;

  /* Temporary buffers used to parse a rule into segments. */
  svn_membuf_t rule_path_buffer;
  svn_stringbuf_t *rule_string_buffer;

  /* The warning callback and its baton. */
  svn_repos_authz_warning_func_t warning_func;
  void *warning_baton;

  /* The parser's scratch pool. This may not be the same pool as
     passed to the constructor callbacks, that is supposed to be an
     iteration pool maintained by the generic parser.

     N.B.: The result pool is AUTHZ->pool. */
  apr_pool_t *parser_pool;
} ctor_baton_t;


/* An empty string with a known address. */
static const char interned_empty_string[] = "";

/* The name of the aliases section. */
static const char aliases_section[] = "aliases";

/* The name of the groups section. */
static const char groups_section[] = "groups";

/* The token indicating that an authz rule contains wildcards. */
static const char glob_rule_token[] = "glob";

/* The anonymous access token. */
static const char anon_access_token[] = "$anonymous";

/* The authenticated access token. */
static const char authn_access_token[] = "$authenticated";

/* Fake token for inverted rights. */
static const char neg_access_token[] = "~~$inverted";

/* Initialize a rights structure.
   The minimum rights start with all available access and are later
   bitwise-and'ed with actual access rights. The maximum rights begin
   empty and are later bitwise-and'ed with actual rights. */
static void init_rights(authz_rights_t *rights)
{
  rights->min_access = authz_access_write;
  rights->max_access = authz_access_none;
 }

/* Initialize a global rights structure.
   The USER string must be interned or statically initialized. */
static void
init_global_rights(authz_global_rights_t *gr, const char *user,
                   apr_pool_t *result_pool)
{
  gr->user = user;
  init_rights(&gr->all_repos_rights);
  init_rights(&gr->any_repos_rights);
  gr->per_repos_rights = apr_hash_make(result_pool);
}


/* Insert the default global ACL into the parsed ACLs. */
static void
insert_default_acl(ctor_baton_t *cb)
{
  parsed_acl_t *acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t);
  acl->acl.sequence_number = 0;
  acl->acl.rule.repos = interned_empty_string;
  acl->acl.rule.len = 0;
  acl->acl.rule.path = NULL;
  acl->acl.anon_access = authz_access_none;
  acl->acl.has_anon_access = TRUE;
  acl->acl.authn_access = authz_access_none;
  acl->acl.has_authn_access = TRUE;
  acl->acl.neg_access = authz_access_none;
  acl->acl.has_neg_access = TRUE;
  acl->acl.user_access = NULL;
  acl->aces = svn_hash__make(cb->parser_pool);
  acl->alias_aces = svn_hash__make(cb->parser_pool);
}


/* Initialize a constuctor baton. */
static ctor_baton_t *
create_ctor_baton(svn_repos_authz_warning_func_t warning_func,
                  void *warning_baton,
                  apr_pool_t *result_pool,
                  apr_pool_t *scratch_pool)
{
  apr_pool_t *const parser_pool = svn_pool_create(scratch_pool);
  ctor_baton_t *const cb = apr_pcalloc(parser_pool, sizeof(*cb));

  authz_full_t *const authz = apr_pcalloc(result_pool, sizeof(*authz));
  init_global_rights(&authz->anon_rights, anon_access_token, result_pool);
  init_global_rights(&authz->authn_rights, authn_access_token, result_pool);
  init_global_rights(&authz->neg_rights, neg_access_token, result_pool);
  authz->user_rights = svn_hash__make(result_pool);
  authz->pool = result_pool;

  cb->authz = authz;
  cb->strings = svn_hash__make(parser_pool);

  cb->sections = svn_hash__make(parser_pool);
  cb->section = NULL;
  cb->parsing_groups = FALSE;
  cb->in_groups = FALSE;
  cb->in_aliases = FALSE;

  cb->parsed_rules = svn_hash__make(parser_pool);
  cb->parsed_groups = svn_hash__make(parser_pool);
  cb->parsed_aliases = svn_hash__make(parser_pool);
  cb->parsed_acls = apr_array_make(parser_pool, 64, sizeof(parsed_acl_t));
  cb->current_acl = NULL;

  svn_membuf__create(&cb->rule_path_buffer, 0, parser_pool);
  cb->rule_string_buffer = svn_stringbuf_create_empty(parser_pool);

  cb->warning_func = warning_func;
  cb->warning_baton = warning_baton;

  cb->parser_pool = parser_pool;

  insert_default_acl(cb);

  return cb;
}


/* Emit a warning. Clears ERROR */
static void
emit_parser_warning(const ctor_baton_t *cb,
                    svn_error_t *error,
                    apr_pool_t *scratch_pool)
{
  if (cb->warning_func)
    cb->warning_func(cb->warning_baton, error, scratch_pool);
  svn_error_clear(error);
}

/* Avoid creating an error struct if there is no warning function. */
#define SVN_AUTHZ_PARSE_WARN(cb, err, pool)     \
  do {                                          \
    if ((cb) && (cb)->warning_func)             \
      emit_parser_warning((cb), (err), (pool)); \
  } while(0)


/* Create and store per-user global rights.
   The USER string must be interned or statically initialized. */
static void
prepare_global_rights(ctor_baton_t *cb, const char *user)
{
  authz_global_rights_t *gr = svn_hash_gets(cb->authz->user_rights, user);
  if (!gr)
    {
      gr = apr_palloc(cb->authz->pool, sizeof(*gr));
      init_global_rights(gr, user, cb->authz->pool);
      svn_hash_sets(cb->authz->user_rights, gr->user, gr);
    }
}


/* Internalize a string that will be referenced by the parsed svn_authz_t.
   If LEN is (apr_size_t)-1, assume the string is NUL-terminated. */
static const char *
intern_string(ctor_baton_t *cb, const char *str, apr_size_t len)
{
  const char *interned;

  if (len == (apr_size_t)-1)
    len = strlen(str);

  interned = apr_hash_get(cb->strings, str, len);
  if (!interned)
    {
      interned = apr_pstrmemdup(cb->authz->pool, str, len);
      apr_hash_set(cb->strings, interned, len, interned);
    }
  return interned;
}


/* Helper for rules_open_section and groups_open_section. */
static svn_error_t *
check_open_section(ctor_baton_t *cb, svn_stringbuf_t *section)
{
  SVN_ERR_ASSERT(!cb->current_acl && !cb->section);
  if (apr_hash_get(cb->sections, section->data, section->len))
    {
      if (cb->parsing_groups)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Section appears more than once"
              " in the global groups file: [%s]"),
            section->data);
      else
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Section appears more than once"
              " in the authz file: [%s]"),
            section->data);
    }

  cb->section = apr_pstrmemdup(cb->parser_pool, section->data, section->len);
  svn_hash_sets(cb->sections,  cb->section, interned_empty_string);
  return SVN_NO_ERROR;
}


/* Constructor callback: Begins the [groups] section. */
static svn_error_t *
groups_open_section(void *baton, svn_stringbuf_t *section)
{
  ctor_baton_t *const cb = baton;

  if (cb->parsing_groups)
    SVN_ERR(check_open_section(cb, section));

  if (0 == strcmp(section->data, groups_section))
    {
      cb->in_groups = TRUE;
      return SVN_NO_ERROR;
    }

  return svn_error_createf(
      SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
      (cb->parsing_groups
       ? _("Section is not valid in the global group file: [%s]")
       : _("Section is not valid in the authz file: [%s]")),
      section->data);
}


/* Constructor callback: Parses a group declaration. */
static svn_error_t *
groups_add_value(void *baton, svn_stringbuf_t *section,
                 svn_stringbuf_t *option, svn_stringbuf_t *value)
{
  ctor_baton_t *const cb = baton;
  const char *group;
  apr_size_t group_len;

  SVN_ERR_ASSERT(cb->in_groups);

  if (strchr("@$&*~", *option->data))
    {
      if (cb->parsing_groups)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Global group name '%s' may not begin with '%c'"),
            option->data, *option->data);
      else
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Group name '%s' may not begin with '%c'"),
            option->data, *option->data);
    }

  /* Decorate the name to make lookups consistent. */
  group = apr_pstrcat(cb->parser_pool, "@", option->data, SVN_VA_NULL);
  group_len = option->len + 1;
  if (apr_hash_get(cb->parsed_groups, group, group_len))
    {
      if (cb->parsing_groups)
        return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                 _("Can't override definition"
                                   " of global group '%s'"),
                                 group);
      else
        return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                 _("Can't override definition"
                                   " of group '%s'"),
                                 group);
    }

  /* We store the whole group definition, so that we can use the
     temporary groups in the baton hash later to fully expand group
     memberships.
     At this point, we can finally internalize the group name. */
  apr_hash_set(cb->parsed_groups,
               intern_string(cb, group, group_len), group_len,
               svn_cstring_split(value->data, ",", TRUE, cb->parser_pool));

  return SVN_NO_ERROR;
}


/* Remove escape sequences in-place. */
static void
unescape_in_place(svn_stringbuf_t *buf)
{
  char *p = buf->data;
  apr_size_t i;

  /* Skip the string up to the first escape sequence. */
  for (i = 0; i < buf->len; ++i)
    {
      if (*p == '\\')
        break;
      ++p;
    }

  if (i < buf->len)
    {
      /* Unescape the remainder of the string. */
      svn_boolean_t escape = TRUE;
      const char *q;

      for (q = p + 1, ++i; i < buf->len; ++i)
        {
          if (escape)
            {
              *p++ = *q++;
              escape = FALSE;
            }
          else if (*q == '\\')
            {
              ++q;
              escape = TRUE;
            }
          else
            *p++ = *q++;
        }

      /* A trailing backslash is literal, so make it part of the pattern. */
      if (escape)
        *p++ = '\\';
      *p = '\0';
      buf->len = p - buf->data;
    }
}


/* Internalize a pattern. */
static void
intern_pattern(ctor_baton_t *cb,
               svn_string_t *pattern,
               const char *string,
               apr_size_t len)
{
  pattern->data = intern_string(cb, string, len);
  pattern->len = len;
}


/* Parse a rule path PATH up to PATH_LEN into *RULE.
   If GLOB is TRUE, treat PATH as possibly containing wildcards.
   SECTION is the whole rule in the authz file.
   Use pools and buffers from CB to do the obvious thing. */
static svn_error_t *
parse_rule_path(authz_rule_t *rule,
                ctor_baton_t *cb,
                svn_boolean_t glob,
                const char *path,
                apr_size_t path_len,
                const char *section)
{
  svn_stringbuf_t *const pattern = cb->rule_string_buffer;
  const char *const path_end = path + path_len;
  authz_rule_segment_t *segment;
  const char *start;
  const char *end;
  int nseg;

  SVN_ERR_ASSERT(*path == '/');

  nseg = 0;
  for (start = path; start != path_end; start = end)
    {
      apr_size_t pattern_len;

      /* Skip the leading slash and find the end of the segment. */
      end = memchr(++start, '/', path_len - 1);
      if (!end)
        end = path_end;

      pattern_len = end - start;
      path_len -= pattern_len + 1;

      if (pattern_len == 0)
        {
          if (nseg == 0)
            {
              /* This is an empty (root) path. */
              rule->len = 0;
              rule->path = NULL;
              return SVN_NO_ERROR;
            }

          /* A path with two consecutive slashes is not canonical. */
          return svn_error_createf(
              SVN_ERR_AUTHZ_INVALID_CONFIG,
              svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                               _("Found empty name in authz rule path")),
              _("Non-canonical path '%s' in authz rule [%s]"),
              path, section);
        }

      /* A path with . or .. segments is not canonical. */
      if (*start == '.'
          && (pattern_len == 1
              || (pattern_len == 2 && start[1] == '.')))
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG,
            (end == start + 1
             ? svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                _("Found '.' in authz rule path"))
             : svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                _("Found '..' in authz rule path"))),
            _("Non-canonical path '%s' in authz rule [%s]"),
            path, section);

      /* Make space for the current segment. */
      ++nseg;
      svn_membuf__resize(&cb->rule_path_buffer, nseg * sizeof(*segment));
      segment = cb->rule_path_buffer.data;
      segment += (nseg - 1);

      if (!glob)
        {
          /* Trivial case: this is not a glob rule, so every segment
             is a literal match. */
          segment->kind = authz_rule_literal;
          intern_pattern(cb, &segment->pattern, start, pattern_len);
          continue;
        }

      /* Copy the segment into the temporary buffer. */
      svn_stringbuf_setempty(pattern);
      svn_stringbuf_appendbytes(pattern, start, pattern_len);

      if (0 == apr_fnmatch_test(pattern->data))
        {
          /* It's a literal match after all. */
          segment->kind = authz_rule_literal;
          unescape_in_place(pattern);
          intern_pattern(cb, &segment->pattern, pattern->data, pattern->len);
          continue;
        }

      if (*pattern->data == '*')
        {
          if (pattern->len == 1
              || (pattern->len == 2 && pattern->data[1] == '*'))
            {
              /* Process * and **, applying normalization as per
                 https://wiki.apache.org/subversion/AuthzImprovements. */

              authz_rule_segment_t *const prev =
                (nseg > 1 ? segment - 1 : NULL);

              if (pattern_len == 1)
                {
                  /* This is a *. Replace **|* with *|**. */
                  if (prev && prev->kind == authz_rule_any_recursive)
                    {
                      prev->kind = authz_rule_any_segment;
                      segment->kind = authz_rule_any_recursive;
                    }
                  else
                    segment->kind = authz_rule_any_segment;
                }
              else
                {
                  /* This is a **. Replace **|** with a single **. */
                  if (prev && prev->kind == authz_rule_any_recursive)
                    {
                      /* Simply drop the redundant new segment. */
                      --nseg;
                      continue;
                    }
                  else
                    segment->kind = authz_rule_any_recursive;
                }

              segment->pattern.data = interned_empty_string;
              segment->pattern.len = 0;
              continue;
            }

          /* Maybe it's a suffix match? */
          if (0 == apr_fnmatch_test(pattern->data + 1))
            {
              svn_stringbuf_leftchop(pattern, 1);
              segment->kind = authz_rule_suffix;
              unescape_in_place(pattern);
              svn_authz__reverse_string(pattern->data, pattern->len);
              intern_pattern(cb, &segment->pattern,
                             pattern->data, pattern->len);
              continue;
            }
        }

      if (pattern->data[pattern->len - 1] == '*')
        {
          /* Might be a prefix match. Note that because of the
             previous test, we already know that the pattern is longer
             than one character. */
          if (pattern->data[pattern->len - 2] != '\\')
            {
              /* OK, the * wasn't  escaped. Chop off the wildcard. */
              svn_stringbuf_chop(pattern, 1);
              if (0 == apr_fnmatch_test(pattern->data))
                {
                  segment->kind = authz_rule_prefix;
                  unescape_in_place(pattern);
                  intern_pattern(cb, &segment->pattern,
                                 pattern->data, pattern->len);
                  continue;
                }

              /* Restore the wildcard since it was not a prefix match. */
              svn_stringbuf_appendbyte(pattern, '*');
            }
        }

      /* It's a generic fnmatch pattern. */
      segment->kind = authz_rule_fnmatch;
      intern_pattern(cb, &segment->pattern, pattern->data, pattern->len);
    }

  SVN_ERR_ASSERT(nseg > 0);

  /* Copy the temporary segments array into the result pool. */
  {
    const apr_size_t path_size = nseg * sizeof(*segment);
    SVN_ERR_ASSERT(path_size <= cb->rule_path_buffer.size);

    rule->len = nseg;
    rule->path = apr_palloc(cb->authz->pool, path_size);
    memcpy(rule->path, cb->rule_path_buffer.data, path_size);
  }

  return SVN_NO_ERROR;
}


/* Check that the parsed RULE is unique within the authz file.
   With the introduction of wildcards, just looking at the SECTION
   names is not sufficient to determine uniqueness.
   Use pools and buffers from CB to do the obvious thing. */
static svn_error_t *
check_unique_rule(ctor_baton_t *cb,
                  const authz_rule_t *rule,
                  const char *section)
{
  svn_stringbuf_t *const buf = cb->rule_string_buffer;
  const char *exists;
  int i;

  /* Construct the key for this rule */
  svn_stringbuf_setempty(buf);
  svn_stringbuf_appendcstr(buf, rule->repos);
  svn_stringbuf_appendbyte(buf, '\n');

  for (i = 0; i < rule->len; ++i)
    {
      authz_rule_segment_t *const seg = &rule->path[i];
      svn_stringbuf_appendbyte(buf, '@' + seg->kind);
      svn_stringbuf_appendbytes(buf, seg->pattern.data, seg->pattern.len);
      svn_stringbuf_appendbyte(buf, '\n');
    }

  /* Check if the section exists. */
  exists = apr_hash_get(cb->parsed_rules, buf->data, buf->len);
  if (exists)
    return svn_error_createf(
        SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
        _("Section [%s] describes the same rule as section [%s]"),
        section, exists);

  /* Insert the rule into the known rules set. */
  apr_hash_set(cb->parsed_rules,
               apr_pstrmemdup(cb->parser_pool, buf->data, buf->len),
               buf->len,
               apr_pstrdup(cb->parser_pool, section));

  return SVN_NO_ERROR;
}


/* Constructor callback: Starts a rule or [aliases] section. */
static svn_error_t *
rules_open_section(void *baton, svn_stringbuf_t *section)
{
  ctor_baton_t *const cb = baton;
  const char *rule = section->data;
  apr_size_t rule_len = section->len;
  svn_boolean_t glob;
  const char *endp;
  parsed_acl_t acl;

  SVN_ERR(check_open_section(cb, section));

  /* Parse rule property tokens. */
  if (*rule != ':')
    glob = FALSE;
  else
    {
      /* This must be a wildcard rule. */
      apr_size_t token_len;

      ++rule; --rule_len;
      endp = memchr(rule, ':', rule_len);
      if (!endp)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Empty repository name in authz rule [%s]"),
            section->data);

      /* Note: the size of glob_rule_token includes the NUL terminator. */
      token_len = endp - rule;
      if (token_len != sizeof(glob_rule_token) - 1
          || memcmp(rule, glob_rule_token, token_len))
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Invalid type token '%s' in authz rule [%s]"),
            apr_pstrmemdup(cb->parser_pool, rule, token_len),
            section->data);

      glob = TRUE;
      rule = endp + 1;
      rule_len -= token_len + 1;
    }

  /* Parse the repository name. */
  endp = (*rule == '/' ? NULL : memchr(rule, ':', rule_len));
  if (!endp)
    acl.acl.rule.repos = interned_empty_string;
  else
    {
      const apr_size_t repos_len = endp - rule;

      /* The rule contains a repository name. */
      if (0 == repos_len)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Empty repository name in authz rule [%s]"),
            section->data);

      acl.acl.rule.repos = intern_string(cb, rule, repos_len);
      rule = endp + 1;
      rule_len -= repos_len + 1;
    }

  /* Parse the actual rule. */
  if (*rule == '/')
    {
      SVN_ERR(parse_rule_path(&acl.acl.rule, cb, glob, rule, rule_len,
                              section->data));
      SVN_ERR(check_unique_rule(cb, &acl.acl.rule, section->data));
    }
  else if (0 == strcmp(section->data, aliases_section))
    {
      cb->in_aliases = TRUE;
      return SVN_NO_ERROR;
    }
  else
    {
      /* This must be the [groups] section. */
      return groups_open_section(cb, section);
    }

  acl.acl.sequence_number = cb->parsed_acls->nelts;
  acl.acl.anon_access = authz_access_none;
  acl.acl.has_anon_access = FALSE;
  acl.acl.authn_access = authz_access_none;
  acl.acl.has_authn_access = FALSE;
  acl.acl.neg_access = authz_access_none;
  acl.acl.has_neg_access = FALSE;
  acl.acl.user_access = NULL;

  acl.aces = svn_hash__make(cb->parser_pool);
  acl.alias_aces = svn_hash__make(cb->parser_pool);

  cb->current_acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t);
  *cb->current_acl = acl;
  return SVN_NO_ERROR;
}


/* Parses an alias declaration. The definition (username) of the
   alias will always be interned. */
static svn_error_t *
add_alias_definition(ctor_baton_t *cb,
                     svn_stringbuf_t *option, svn_stringbuf_t *value)
{
  const char *alias;
  apr_size_t alias_len;
  const char *user;

  if (strchr("@$&*~", *option->data))
    return svn_error_createf(
        SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
        _("Alias name '%s' may not begin with '%c'"),
        option->data, *option->data);

  /* Decorate the name to make lookups consistent. */
  alias = apr_pstrcat(cb->parser_pool, "&", option->data, SVN_VA_NULL);
  alias_len = option->len + 1;
  if (apr_hash_get(cb->parsed_aliases, alias, alias_len))
    return svn_error_createf(
        SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
        _("Can't override definition of alias '%s'"),
        alias);

  user = intern_string(cb, value->data, value->len);
  apr_hash_set(cb->parsed_aliases, alias, alias_len, user);

  /* Prepare the global rights struct for this user. */
  prepare_global_rights(cb, user);
  return SVN_NO_ERROR;
}

/* Parses an access entry. Groups and users in access entry names will
   always be interned, aliases will never be. */
static svn_error_t *
add_access_entry(ctor_baton_t *cb, svn_stringbuf_t *section,
                 svn_stringbuf_t *option, svn_stringbuf_t *value)
{
  parsed_acl_t *const acl = cb->current_acl;
  const char *name = option->data;
  apr_size_t name_len = option->len;
  const svn_boolean_t inverted = (*name == '~');
  svn_boolean_t anonymous = FALSE;
  svn_boolean_t authenticated = FALSE;
  authz_access_t access = authz_access_none;
  authz_ace_t *ace;
  int i;

  SVN_ERR_ASSERT(acl != NULL);

  if (inverted)
    {
      ++name;
      --name_len;
    }

  /* Determine the access entry type. */
  switch (*name)
    {
    case '~':
      return svn_error_createf(
          SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
          _("Access entry '%s' has more than one inversion;"
            " double negatives are not permitted"),
          option->data);
      break;

    case '*':
      if (name_len != 1)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Access entry '%s' is not valid;"
              " it must be a single '*'"),
            option->data);

      if (inverted)
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Access entry '~*' will never match"));

      anonymous = TRUE;
      authenticated = TRUE;
      break;

    case '$':
      if (0 == strcmp(name, anon_access_token))
        {
          if (inverted)
            authenticated = TRUE;
          else
            anonymous = TRUE;
        }
      else if (0 == strcmp(name, authn_access_token))
        {
          if (inverted)
            anonymous = TRUE;
          else
            authenticated = TRUE;
        }
      else
        return svn_error_createf(
            SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
            _("Access entry token '%s' is not valid;"
              " should be '%s' or '%s'"),
            option->data, anon_access_token, authn_access_token);
      break;

    default:
      /* A username, group name or alias. */;
    }

  /* Parse the access rights. */
  for (i = 0; i < value->len; ++i)
    {
      const char access_code = value->data[i];
      switch (access_code)
        {
        case 'r':
          access |= authz_access_read_flag;
          break;

        case 'w':
          access |= authz_access_write_flag;
          break;

        default:
          if (!svn_ctype_isspace(access_code))
            return svn_error_createf(
                SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                _("The access mode '%c' in access entry '%s'"
                  " of rule [%s] is not valid"),
                access_code, option->data, section->data);
      }
    }

  /* We do not support write-only access. */
  if ((access & authz_access_write_flag) && !(access & authz_access_read_flag))
    return svn_error_createf(
        SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
        _("Write-only access entry '%s' of rule [%s] is not valid"),
        option->data, section->data);

  /* Update the parsed ACL with this access entry. */
  if (anonymous || authenticated)
    {
      if (anonymous)
        {
          acl->acl.has_anon_access = TRUE;
          acl->acl.anon_access |= access;
        }
      if (authenticated)
        {
          acl->acl.has_authn_access = TRUE;
          acl->acl.authn_access |= access;
        }
    }
  else
    {
      /* The inversion tag must be part of the key in the hash
         table, otherwise we can't tell regular and inverted
         entries appart. */
      const char *key = (inverted ? name - 1 : name);
      const apr_size_t key_len = (inverted ? name_len + 1 : name_len);
      const svn_boolean_t aliased = (*name == '&');
      apr_hash_t *aces = (aliased ? acl->alias_aces : acl->aces);

      ace = apr_hash_get(aces, key, key_len);
      if (ace)
        ace->access |= access;
      else
        {
          ace = apr_palloc(cb->parser_pool, sizeof(*ace));
          ace->name = (aliased
                       ? apr_pstrmemdup(cb->parser_pool, name, name_len)
                       : intern_string(cb, name, name_len));
          ace->members = NULL;
          ace->inverted = inverted;
          ace->access = access;

          key = (inverted
                 ? apr_pstrmemdup(cb->parser_pool, key, key_len)
                 : ace->name);
          apr_hash_set(aces, key, key_len, ace);

          /* Prepare the global rights struct for this user. */
          if (!aliased && *ace->name != '@')
            prepare_global_rights(cb, ace->name);
        }

      /* Propagate rights for inverted selectors to the global rights, otherwise
         an access check can bail out early. See: SVN-4793 */
      if (inverted)
        {
          acl->acl.has_neg_access = TRUE;
          acl->acl.neg_access |= access;
        }
    }

  return SVN_NO_ERROR;
}

/* Constructor callback: Parse a rule, alias or group delcaration. */
static svn_error_t *
rules_add_value(void *baton, svn_stringbuf_t *section,
                svn_stringbuf_t *option, svn_stringbuf_t *value)
{
  ctor_baton_t *const cb = baton;

  if (cb->in_groups)
    return groups_add_value(baton, section, option, value);

  if (cb->in_aliases)
    return add_alias_definition(cb, option, value);

  return add_access_entry(cb, section, option, value);
}


/* Constructor callback: Close a section. */
static svn_error_t *
close_section(void *baton, svn_stringbuf_t *section)
{
  ctor_baton_t *const cb = baton;

  SVN_ERR_ASSERT(0 == strcmp(cb->section, section->data));
  cb->section = NULL;
  cb->current_acl = NULL;
  cb->in_groups = FALSE;
  cb->in_aliases = FALSE;
  return SVN_NO_ERROR;
}


/* Add a user to GROUP.
   GROUP is never internalized, but USER always is.
   Adding a NULL user will create an empty group, if it doesn't exist. */
static void
add_to_group(ctor_baton_t *cb, const char *group, const char *user)
{
  apr_hash_t *members = svn_hash_gets(cb->expanded_groups, group);
  if (!members)
    {
      group = intern_string(cb, group, -1);
      members = svn_hash__make(cb->authz->pool);
      svn_hash_sets(cb->expanded_groups, group, members);
    }
  if (user)
    svn_hash_sets(members, user, interned_empty_string);
}


/* Hash iterator for expanding group definitions.
   WARNING: This function is recursive! */
static svn_error_t *
expand_group_callback(void *baton,
                      const void *key,
                      apr_ssize_t klen,
                      void *value,
                      apr_pool_t *scratch_pool)
{
  ctor_baton_t *const cb = baton;
  const char *const group = key;
  apr_array_header_t *members = value;
  int i;

  if (0 == members->nelts)
    {
      /* Create the group with no members. */
      add_to_group(cb, group, NULL);
      return SVN_NO_ERROR;
    }

  for (i = 0; i < members->nelts; ++i)
    {
      const char *member = APR_ARRAY_IDX(members, i, const char*);
      if (0 == strcmp(member, group))
            return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                     _("Recursive definition of group '%s'"),
                                     group);

      if (*member == '&')
        {
          /* Add expanded alias to the group.
             N.B.: the user name is already internalized. */
          const char *user = svn_hash_gets(cb->parsed_aliases, member);
          if (!user)
            return svn_error_createf(
                SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                _("Alias '%s' was never defined"),
                member);

          add_to_group(cb, group, user);
        }
      else if (*member != '@')
        {
          /* Add the member to the group. */
          const char *user = intern_string(cb, member, -1);
          add_to_group(cb, group, user);

          /* Prepare the global rights struct for this user. */
          prepare_global_rights(cb, user);
        }
      else
        {
          /* Recursively expand the group membership */
          apr_array_header_t *member_members
            = svn_hash_gets(cb->parsed_groups, member);
          if (!member_members)
            return svn_error_createf(
                SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                _("Undefined group '%s'"),
                member);
          SVN_ERR(expand_group_callback(cb, key, klen,
                                        member_members, scratch_pool));
        }
    }
  return SVN_NO_ERROR;
}


/* Hash iteration baton for merge_alias_ace. */
typedef struct merge_alias_baton_t
{
  apr_hash_t *aces;
  ctor_baton_t *cb;
} merge_alias_baton_t;

/* Hash iterator for expanding and mergina alias-based ACEs
   into the user/group-based ACEs. */
static svn_error_t *
merge_alias_ace(void *baton,
                const void *key,
                apr_ssize_t klen,
                void *value,
                apr_pool_t *scratch_pool)
{
  merge_alias_baton_t *const mab = baton;
  authz_ace_t *aliased_ace = value;
  const char *alias = aliased_ace->name;
  const char *unaliased_key;
  const char *user;
  authz_ace_t *ace;

  user = svn_hash_gets(mab->cb->parsed_aliases, alias);
  if (!user)
    return svn_error_createf(
        SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
        _("Alias '%s' was never defined"),
        alias);

  /* N.B.: The user name is always internalized,
     but the inverted key may not be. */
  if (!aliased_ace->inverted)
    unaliased_key = user;
  else
    {
      unaliased_key = apr_pstrcat(mab->cb->parser_pool,
                                  "~", user, SVN_VA_NULL);
      unaliased_key = intern_string(mab->cb, unaliased_key, -1);
    }

  ace = svn_hash_gets(mab->aces, unaliased_key);
  if (!ace)
    {
      aliased_ace->name = user;
      svn_hash_sets(mab->aces, unaliased_key, aliased_ace);
    }
  else
    {
      SVN_ERR_ASSERT(!ace->inverted == !aliased_ace->inverted);
      ace->access |= aliased_ace->access;
    }

  return SVN_NO_ERROR;
}


/* Hash iteration baton for array_insert_ace. */
typedef struct insert_ace_baton_t
{
  apr_array_header_t *ace_array;
  ctor_baton_t *cb;
} insert_ace_baton_t;

/* Hash iterator, inserts an ACE into the ACLs array. */
static svn_error_t *
array_insert_ace(void *baton,
                 const void *key,
                 apr_ssize_t klen,
                 void *value,
                 apr_pool_t *scratch_pool)
{
  insert_ace_baton_t *iab = baton;
  authz_ace_t *ace = value;

  /* Add group membership info to the ACE. */
  if (*ace->name == '@')
    {
      SVN_ERR_ASSERT(ace->members == NULL);
      ace->members = svn_hash_gets(iab->cb->expanded_groups, ace->name);
      if (!ace->members)
        {
          return svn_error_createf(
              SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
              _("Access entry refers to undefined group '%s'"),
              ace->name);
        }
      else if (0 == apr_hash_count(ace->members))
        {
          /* An ACE for an empty group has no effect, so ignore it. */
          SVN_AUTHZ_PARSE_WARN(
              iab->cb,
              svn_error_createf(
                  SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                  _("Ignoring access entry for empty group '%s'"),
                  ace->name),
              scratch_pool);
          return SVN_NO_ERROR;
        }
    }

  APR_ARRAY_PUSH(iab->ace_array, authz_ace_t) = *ace;
  return SVN_NO_ERROR;
}


/* Update accumulated RIGHTS from ACCESS. */
static void
update_rights(authz_rights_t *rights,
              authz_access_t access)
{
  rights->min_access &= access;
  rights->max_access |= access;
}


/* Update a global RIGHTS based on REPOS and ACCESS. */
static void
update_global_rights(authz_global_rights_t *gr,
                     const char *repos,
                     authz_access_t access)
{
  update_rights(&gr->all_repos_rights, access);
  if (0 == strcmp(repos, AUTHZ_ANY_REPOSITORY))
    update_rights(&gr->any_repos_rights, access);
  else
    {
      authz_rights_t *rights = svn_hash_gets(gr->per_repos_rights, repos);
      if (rights)
        update_rights(rights, access);
      else
        {
          rights = apr_palloc(apr_hash_pool_get(gr->per_repos_rights),
                              sizeof(*rights));
          init_rights(rights);
          update_rights(rights, access);
          svn_hash_sets(gr->per_repos_rights, repos, rights);
        }
    }
}


/* Hash iterator to update global per-user rights from an ACL. */
static svn_error_t *
update_user_rights(void *baton,
                   const void *key,
                   apr_ssize_t klen,
                   void *value,
                   apr_pool_t *scratch_pool)
{
  const authz_acl_t *const acl = baton;
  const char *const user = key;
  authz_global_rights_t *const gr = value;
  authz_access_t access;
  svn_boolean_t has_access =
    svn_authz__get_acl_access(&access, acl, user, acl->rule.repos);

  if (has_access)
    update_global_rights(gr, acl->rule.repos, access);
  return SVN_NO_ERROR;
}


/* List iterator, expands/merges a parsed ACL into its final form and
   appends it to the authz info's ACL array. */
static svn_error_t *
expand_acl_callback(void *baton,
                    void *item,
                    apr_pool_t *scratch_pool)
{
  ctor_baton_t *const cb = baton;
  parsed_acl_t *const pacl = item;
  authz_acl_t *const acl = &pacl->acl;

  /* Expand and merge the aliased ACEs. */
  if (apr_hash_count(pacl->alias_aces))
    {
      merge_alias_baton_t mab;
      mab.aces = pacl->aces;
      mab.cb = cb;
      SVN_ERR(svn_iter_apr_hash(NULL, pacl->alias_aces,
                                merge_alias_ace, &mab, scratch_pool));
    }

  /* Make an array from the merged hashes. */
  acl->user_access =
    apr_array_make(cb->authz->pool, apr_hash_count(pacl->aces),
                   sizeof(authz_ace_t));
  {
    insert_ace_baton_t iab;
    iab.ace_array = acl->user_access;
    iab.cb = cb;
    SVN_ERR(svn_iter_apr_hash(NULL, pacl->aces,
                              array_insert_ace, &iab, scratch_pool));
  }

  /* Store the completed ACL into authz. */
  APR_ARRAY_PUSH(cb->authz->acls, authz_acl_t) = *acl;

  /* Update global access rights for this ACL. */
  if (acl->has_anon_access)
    {
      cb->authz->has_anon_rights = TRUE;
      update_global_rights(&cb->authz->anon_rights,
                           acl->rule.repos, acl->anon_access);
    }
  if (acl->has_authn_access)
    {
      cb->authz->has_authn_rights = TRUE;
      update_global_rights(&cb->authz->authn_rights,
                           acl->rule.repos, acl->authn_access);
    }
  if (acl->has_neg_access)
    {
      cb->authz->has_neg_rights = TRUE;
      update_global_rights(&cb->authz->neg_rights,
                           acl->rule.repos, acl->neg_access);
    }
  SVN_ERR(svn_iter_apr_hash(NULL, cb->authz->user_rights,
                            update_user_rights, acl, scratch_pool));
  return SVN_NO_ERROR;
}


/* Compare two ACLs in rule lexical order, then repository order, then
   order of definition. This ensures that our default ACL is always
   first in the sorted array. */
static int
compare_parsed_acls(const void *va, const void *vb)
{
  const parsed_acl_t *const a = va;
  const parsed_acl_t *const b = vb;

  int cmp = svn_authz__compare_rules(&a->acl.rule, &b->acl.rule);
  if (cmp == 0)
    cmp = a->acl.sequence_number - b->acl.sequence_number;
  return cmp;
}


svn_error_t *
svn_authz__parse(authz_full_t **authz,
                 svn_stream_t *rules,
                 svn_stream_t *groups,
                 svn_repos_authz_warning_func_t warning_func,
                 void *warning_baton,
                 apr_pool_t *result_pool,
                 apr_pool_t *scratch_pool)
{
  ctor_baton_t *const cb = create_ctor_baton(warning_func, warning_baton,
                                             result_pool, scratch_pool);

  /*
   * Pass 1: Parse the authz file.
   */
  SVN_ERR(svn_config__parse_stream(rules,
                                   svn_config__constructor_create(
                                       rules_open_section,
                                       close_section,
                                       rules_add_value,
                                       cb->parser_pool),
                                   cb, cb->parser_pool));

  /*
   * Pass 1.6487: Parse the global groups file.
   */
  if (groups)
    {
      /* Check that the authz file did not contain any groups. */
      if (0 != apr_hash_count(cb->parsed_groups))
          return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
                                  ("Authz file cannot contain any groups"
                                   " when global groups are being used."));

      apr_hash_clear(cb->sections);
      cb->parsing_groups = TRUE;
      SVN_ERR(svn_config__parse_stream(groups,
                                       svn_config__constructor_create(
                                           groups_open_section,
                                           close_section,
                                           groups_add_value,
                                           cb->parser_pool),
                                       cb, cb->parser_pool));
    }

  /*
   * Pass 2: Expand groups and construct the final svn_authz_t.
   */
  cb->expanded_groups = svn_hash__make(cb->parser_pool);
  SVN_ERR(svn_iter_apr_hash(NULL, cb->parsed_groups,
                            expand_group_callback, cb, cb->parser_pool));


  /* Sort the parsed ACLs in rule lexical order and pop off the
     default global ACL iff an equivalent ACL was defined in the authz
     file. */
  if (cb->parsed_acls->nelts > 1)
    {
      parsed_acl_t *defacl;
      parsed_acl_t *nxtacl;

      svn_sort__array(cb->parsed_acls, compare_parsed_acls);
      defacl = &APR_ARRAY_IDX(cb->parsed_acls, 0, parsed_acl_t);
      nxtacl = &APR_ARRAY_IDX(cb->parsed_acls, 1, parsed_acl_t);

      /* If the first ACL is not our default thingamajig, there's a
         bug in our comparator. */
      SVN_ERR_ASSERT(
          defacl->acl.sequence_number == 0 && defacl->acl.rule.len == 0
          && 0 == strcmp(defacl->acl.rule.repos, AUTHZ_ANY_REPOSITORY));

      /* Pop the default ACL off the array if another equivalent
         exists, after merging the default rights. */
      if (0 == svn_authz__compare_rules(&defacl->acl.rule, &nxtacl->acl.rule))
        {
          nxtacl->acl.has_anon_access = TRUE;
          nxtacl->acl.has_authn_access = TRUE;
          cb->parsed_acls->elts = (char*)(nxtacl);
          --cb->parsed_acls->nelts;
        }
    }

  cb->authz->acls = apr_array_make(cb->authz->pool, cb->parsed_acls->nelts,
                                   sizeof(authz_acl_t));
  SVN_ERR(svn_iter_apr_array(NULL, cb->parsed_acls,
                             expand_acl_callback, cb, cb->parser_pool));

  *authz = cb->authz;
  apr_pool_destroy(cb->parser_pool);
  return SVN_NO_ERROR;
}


void
svn_authz__reverse_string(char *string, apr_size_t len)
{
  char *left = string;
  char *right = string + len - 1;
  for (; left < right; ++left, --right)
    {
      char c = *left;
      *left = *right;
      *right = c;
    }
}


int
svn_authz__compare_paths(const authz_rule_t *a, const authz_rule_t *b)
{
  const int min_len = (a->len > b->len ? b->len : a->len);
  int i;

  for (i = 0; i < min_len; ++i)
    {
      int cmp = a->path[i].kind - b->path[i].kind;
      if (0 == cmp)
        {
          const char *const aseg = a->path[i].pattern.data;
          const char *const bseg = b->path[i].pattern.data;

          /* Exploit the fact that segment patterns are interned. */
          if (aseg != bseg)
            cmp = strcmp(aseg, bseg);
          else
            cmp = 0;
        }
      if (0 != cmp)
        return cmp;
    }

  /* Sort shorter rules first. */
  if (a->len != b->len)
    return a->len - b->len;

  return 0;
}

int
svn_authz__compare_rules(const authz_rule_t *a, const authz_rule_t *b)
{
  int diff = svn_authz__compare_paths(a, b);
  if (diff)
    return diff;

  /* Repository names are interned, too. */
  if (a->repos != b->repos)
    return strcmp(a->repos, b->repos);

  return 0;
}
