]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - contrib/subversion/subversion/libsvn_repos/authz_parse.c
Update svn-1.9.7 to 1.10.0.
[FreeBSD/FreeBSD.git] / contrib / subversion / subversion / libsvn_repos / authz_parse.c
1 /* authz_parse.c : Parser for path-based access control
2  *
3  * ====================================================================
4  *    Licensed to the Apache Software Foundation (ASF) under one
5  *    or more contributor license agreements.  See the NOTICE file
6  *    distributed with this work for additional information
7  *    regarding copyright ownership.  The ASF licenses this file
8  *    to you under the Apache License, Version 2.0 (the
9  *    "License"); you may not use this file except in compliance
10  *    with the License.  You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *    Unless required by applicable law or agreed to in writing,
15  *    software distributed under the License is distributed on an
16  *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  *    KIND, either express or implied.  See the License for the
18  *    specific language governing permissions and limitations
19  *    under the License.
20  * ====================================================================
21  */
22
23 #include <apr_fnmatch.h>
24 #include <apr_tables.h>
25
26 #include "svn_ctype.h"
27 #include "svn_error.h"
28 #include "svn_hash.h"
29 #include "svn_iter.h"
30 #include "svn_pools.h"
31 #include "svn_repos.h"
32
33 #include "private/svn_fspath.h"
34 #include "private/svn_config_private.h"
35 #include "private/svn_sorts_private.h"
36 #include "private/svn_string_private.h"
37 #include "private/svn_subr_private.h"
38
39 #include "svn_private_config.h"
40
41 #include "authz.h"
42
43
44 /* Temporary ACL constructed by the parser. */
45 typedef struct parsed_acl_t
46 {
47   /* The global ACL.
48      The strings in ACL.rule are allocated from the result pool.
49      ACL.user_access is null during the parsing stage. */
50   authz_acl_t acl;
51
52   /* The set of access control entries. In the second pass, aliases in
53      these entries will be expanded and equivalent entries will be
54      merged. The entries are allocated from the parser pool. */
55   apr_hash_t *aces;
56
57   /* The set of access control entries that use aliases. In the second
58      pass, aliases in these entries will be expanded and merged into ACES.
59      The entries are allocated from the parser pool. */
60   apr_hash_t *alias_aces;
61 } parsed_acl_t;
62
63
64 /* Temporary group definition constructed by the authz/group parser.
65    Once all groups and aliases are defined, a second pass over these
66    data will recursively expand group memberships. */
67 typedef struct parsed_group_t
68 {
69   svn_boolean_t local_group;
70   apr_array_header_t *members;
71 } parsed_group_t;
72
73
74 /* Baton for the parser constructor. */
75 typedef struct ctor_baton_t
76 {
77   /* The final output of the parser. */
78   authz_full_t *authz;
79
80   /* Interned-string set, allocated in AUTHZ->pool.
81      Stores singleton instances of user, group and repository names,
82      which are used by members of the AUTHZ structure. By reusing the
83      same immutable string multiple times, we reduce the size of the
84      authz representation in the result pool.
85
86      N.B.: Whilst the strings are allocated from teh result pool, the
87      hash table itself is not. */
88   apr_hash_t *strings;
89
90   /* A set of all the sections that were seen in the authz or global
91      groups file. Rules, aliases and groups may each only be defined
92      once in the authz file. The global groups file may only contain a
93      [groups] section. */
94   apr_hash_t *sections;
95
96   /* The name of the section we're currently parsing. */
97   const char *section;
98
99   /* TRUE iff we're parsing the global groups file. */
100   svn_boolean_t parsing_groups;
101
102   /* TRUE iff we're parsing a [groups] section. */
103   svn_boolean_t in_groups;
104
105   /* TRUE iff we're parsing an [aliases] section. */
106   svn_boolean_t in_aliases;
107
108   /* A set of all the unique rules we parsed from the section names. */
109   apr_hash_t *parsed_rules;
110
111   /* Temporary parsed-groups definitions. */
112   apr_hash_t *parsed_groups;
113
114   /* Temporary alias mappings. */
115   apr_hash_t *parsed_aliases;
116
117   /* Temporary parsed-acl definitions. */
118   apr_array_header_t *parsed_acls;
119
120   /* Temporary expanded groups definitions. */
121   apr_hash_t *expanded_groups;
122
123   /* The temporary ACL we're currently constructing. */
124   parsed_acl_t *current_acl;
125
126   /* Temporary buffers used to parse a rule into segments. */
127   svn_membuf_t rule_path_buffer;
128   svn_stringbuf_t *rule_string_buffer;
129
130   /* The parser's scratch pool. This may not be the same pool as
131      passed to the constructor callbacks, that is supposed to be an
132      iteration pool maintained by the generic parser.
133
134      N.B.: The result pool is AUTHZ->pool. */
135   apr_pool_t *parser_pool;
136 } ctor_baton_t;
137
138
139 /* An empty string with a known address. */
140 static const char interned_empty_string[] = "";
141
142 /* The name of the aliases section. */
143 static const char aliases_section[] = "aliases";
144
145 /* The name of the groups section. */
146 static const char groups_section[] = "groups";
147
148 /* The token indicating that an authz rule contains wildcards. */
149 static const char glob_rule_token[] = "glob";
150
151 /* The anonymous access token. */
152 static const char anon_access_token[] = "$anonymous";
153
154 /* The authenticated access token. */
155 static const char authn_access_token[] = "$authenticated";
156
157
158 /* Initialize a rights structure.
159    The minimum rights start with all available access and are later
160    bitwise-and'ed with actual access rights. The maximum rights begin
161    empty and are later bitwise-and'ed with actual rights. */
162 static void init_rights(authz_rights_t *rights)
163 {
164   rights->min_access = authz_access_write;
165   rights->max_access = authz_access_none;
166  }
167
168 /* Initialize a global rights structure.
169    The USER string must be interned or statically initialized. */
170 static void
171 init_global_rights(authz_global_rights_t *gr, const char *user,
172                    apr_pool_t *result_pool)
173 {
174   gr->user = user;
175   init_rights(&gr->all_repos_rights);
176   init_rights(&gr->any_repos_rights);
177   gr->per_repos_rights = apr_hash_make(result_pool);
178 }
179
180
181 /* Insert the default global ACL into the parsed ACLs. */
182 static void
183 insert_default_acl(ctor_baton_t *cb)
184 {
185   parsed_acl_t *acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t);
186   acl->acl.sequence_number = 0;
187   acl->acl.rule.repos = interned_empty_string;
188   acl->acl.rule.len = 0;
189   acl->acl.rule.path = NULL;
190   acl->acl.anon_access = authz_access_none;
191   acl->acl.has_anon_access = TRUE;
192   acl->acl.authn_access = authz_access_none;
193   acl->acl.has_authn_access = TRUE;
194   acl->acl.user_access = NULL;
195   acl->aces = svn_hash__make(cb->parser_pool);
196   acl->alias_aces = svn_hash__make(cb->parser_pool);
197 }
198
199
200 /* Initialize a constuctor baton. */
201 static ctor_baton_t *
202 create_ctor_baton(apr_pool_t *result_pool,
203                   apr_pool_t *scratch_pool)
204 {
205   apr_pool_t *const parser_pool = svn_pool_create(scratch_pool);
206   ctor_baton_t *const cb = apr_pcalloc(parser_pool, sizeof(*cb));
207
208   authz_full_t *const authz = apr_pcalloc(result_pool, sizeof(*authz));
209   init_global_rights(&authz->anon_rights, anon_access_token, result_pool);
210   init_global_rights(&authz->authn_rights, authn_access_token, result_pool);
211   authz->user_rights = svn_hash__make(result_pool);
212   authz->pool = result_pool;
213
214   cb->authz = authz;
215   cb->strings = svn_hash__make(parser_pool);
216
217   cb->sections = svn_hash__make(parser_pool);
218   cb->section = NULL;
219   cb->parsing_groups = FALSE;
220   cb->in_groups = FALSE;
221   cb->in_aliases = FALSE;
222
223   cb->parsed_rules = svn_hash__make(parser_pool);
224   cb->parsed_groups = svn_hash__make(parser_pool);
225   cb->parsed_aliases = svn_hash__make(parser_pool);
226   cb->parsed_acls = apr_array_make(parser_pool, 64, sizeof(parsed_acl_t));
227   cb->current_acl = NULL;
228
229   svn_membuf__create(&cb->rule_path_buffer, 0, parser_pool);
230   cb->rule_string_buffer = svn_stringbuf_create_empty(parser_pool);
231
232   cb->parser_pool = parser_pool;
233
234   insert_default_acl(cb);
235
236   return cb;
237 }
238
239
240 /* Create and store per-user global rights.
241    The USER string must be interned or statically initialized. */
242 static void
243 prepare_global_rights(ctor_baton_t *cb, const char *user)
244 {
245   authz_global_rights_t *gr = svn_hash_gets(cb->authz->user_rights, user);
246   if (!gr)
247     {
248       gr = apr_palloc(cb->authz->pool, sizeof(*gr));
249       init_global_rights(gr, user, cb->authz->pool);
250       svn_hash_sets(cb->authz->user_rights, gr->user, gr);
251     }
252 }
253
254
255 /* Internalize a string that will be referenced by the parsed svn_authz_t.
256    If LEN is (apr_size_t)-1, assume the string is NUL-terminated. */
257 static const char *
258 intern_string(ctor_baton_t *cb, const char *str, apr_size_t len)
259 {
260   const char *interned;
261
262   if (len == (apr_size_t)-1)
263     len = strlen(str);
264
265   interned = apr_hash_get(cb->strings, str, len);
266   if (!interned)
267     {
268       interned = apr_pstrmemdup(cb->authz->pool, str, len);
269       apr_hash_set(cb->strings, interned, len, interned);
270     }
271   return interned;
272 }
273
274
275 /* Helper for rules_open_section and groups_open_section. */
276 static svn_error_t *
277 check_open_section(ctor_baton_t *cb, svn_stringbuf_t *section)
278 {
279   SVN_ERR_ASSERT(!cb->current_acl && !cb->section);
280   if (apr_hash_get(cb->sections, section->data, section->len))
281     {
282       if (cb->parsing_groups)
283         return svn_error_createf(
284             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
285             _("Section appears more than once"
286               " in the global groups file: [%s]"),
287             section->data);
288       else
289         return svn_error_createf(
290             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
291             _("Section appears more than once"
292               " in the authz file: [%s]"),
293             section->data);
294     }
295
296   cb->section = apr_pstrmemdup(cb->parser_pool, section->data, section->len);
297   svn_hash_sets(cb->sections,  cb->section, interned_empty_string);
298   return SVN_NO_ERROR;
299 }
300
301
302 /* Constructor callback: Begins the [groups] section. */
303 static svn_error_t *
304 groups_open_section(void *baton, svn_stringbuf_t *section)
305 {
306   ctor_baton_t *const cb = baton;
307
308   if (cb->parsing_groups)
309     SVN_ERR(check_open_section(cb, section));
310
311   if (0 == strcmp(section->data, groups_section))
312     {
313       cb->in_groups = TRUE;
314       return SVN_NO_ERROR;
315     }
316
317   return svn_error_createf(
318       SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
319       (cb->parsing_groups
320        ? _("Section is not valid in the global group file: [%s]")
321        : _("Section is not valid in the authz file: [%s]")),
322       section->data);
323 }
324
325
326 /* Constructor callback: Parses a group declaration. */
327 static svn_error_t *
328 groups_add_value(void *baton, svn_stringbuf_t *section,
329                  svn_stringbuf_t *option, svn_stringbuf_t *value)
330 {
331   ctor_baton_t *const cb = baton;
332   const char *group;
333   apr_size_t group_len;
334
335   SVN_ERR_ASSERT(cb->in_groups);
336
337   if (strchr("@$&*~", *option->data))
338     {
339       if (cb->parsing_groups)
340         return svn_error_createf(
341             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
342             _("Global group name '%s' may not begin with '%c'"),
343             option->data, *option->data);
344       else
345         return svn_error_createf(
346             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
347             _("Group name '%s' may not begin with '%c'"),
348             option->data, *option->data);
349     }
350
351   /* Decorate the name to make lookups consistent. */
352   group = apr_pstrcat(cb->parser_pool, "@", option->data, SVN_VA_NULL);
353   group_len = option->len + 1;
354   if (apr_hash_get(cb->parsed_groups, group, group_len))
355     {
356       if (cb->parsing_groups)
357         return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
358                                  _("Can't override definition"
359                                    " of global group '%s'"),
360                                  group);
361       else
362         return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
363                                  _("Can't override definition"
364                                    " of group '%s'"),
365                                  group);
366     }
367
368   /* We store the whole group definition, so that we can use the
369      temporary groups in the baton hash later to fully expand group
370      memberships.
371      At this point, we can finally internalize the group name. */
372   apr_hash_set(cb->parsed_groups,
373                intern_string(cb, group, group_len), group_len,
374                svn_cstring_split(value->data, ",", TRUE, cb->parser_pool));
375
376   return SVN_NO_ERROR;
377 }
378
379
380 /* Remove escape sequences in-place. */
381 static void
382 unescape_in_place(svn_stringbuf_t *buf)
383 {
384   char *p = buf->data;
385   apr_size_t i;
386
387   /* Skip the string up to the first escape sequence. */
388   for (i = 0; i < buf->len; ++i)
389     {
390       if (*p == '\\')
391         break;
392       ++p;
393     }
394
395   if (i < buf->len)
396     {
397       /* Unescape the remainder of the string. */
398       svn_boolean_t escape = TRUE;
399       const char *q;
400
401       for (q = p + 1, ++i; i < buf->len; ++i)
402         {
403           if (escape)
404             {
405               *p++ = *q++;
406               escape = FALSE;
407             }
408           else if (*q == '\\')
409             {
410               ++q;
411               escape = TRUE;
412             }
413           else
414             *p++ = *q++;
415         }
416
417       /* A trailing backslash is literal, so make it part of the pattern. */
418       if (escape)
419         *p++ = '\\';
420       *p = '\0';
421       buf->len = p - buf->data;
422     }
423 }
424
425
426 /* Internalize a pattern. */
427 static void
428 intern_pattern(ctor_baton_t *cb,
429                svn_string_t *pattern,
430                const char *string,
431                apr_size_t len)
432 {
433   pattern->data = intern_string(cb, string, len);
434   pattern->len = len;
435 }
436
437
438 /* Parse a rule path PATH up to PATH_LEN into *RULE.
439    If GLOB is TRUE, treat PATH as possibly containing wildcards.
440    SECTION is the whole rule in the authz file.
441    Use pools and buffers from CB to do the obvious thing. */
442 static svn_error_t *
443 parse_rule_path(authz_rule_t *rule,
444                 ctor_baton_t *cb,
445                 svn_boolean_t glob,
446                 const char *path,
447                 apr_size_t path_len,
448                 const char *section)
449 {
450   svn_stringbuf_t *const pattern = cb->rule_string_buffer;
451   const char *const path_end = path + path_len;
452   authz_rule_segment_t *segment;
453   const char *start;
454   const char *end;
455   int nseg;
456
457   SVN_ERR_ASSERT(*path == '/');
458
459   nseg = 0;
460   for (start = path; start != path_end; start = end)
461     {
462       apr_size_t pattern_len;
463
464       /* Skip the leading slash and find the end of the segment. */
465       end = memchr(++start, '/', path_len - 1);
466       if (!end)
467         end = path_end;
468
469       pattern_len = end - start;
470       path_len -= pattern_len + 1;
471
472       if (pattern_len == 0)
473         {
474           if (nseg == 0)
475             {
476               /* This is an empty (root) path. */
477               rule->len = 0;
478               rule->path = NULL;
479               return SVN_NO_ERROR;
480             }
481
482           /* A path with two consecutive slashes is not canonical. */
483           return svn_error_createf(
484               SVN_ERR_AUTHZ_INVALID_CONFIG,
485               svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
486                                _("Found empty name in authz rule path")),
487               _("Non-canonical path '%s' in authz rule [%s]"),
488               path, section);
489         }
490
491       /* A path with . or .. segments is not canonical. */
492       if (*start == '.'
493           && (pattern_len == 1
494               || (pattern_len == 2 && start[1] == '.')))
495         return svn_error_createf(
496             SVN_ERR_AUTHZ_INVALID_CONFIG,
497             (end == start + 1
498              ? svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
499                                 _("Found '.' in authz rule path"))
500              : svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
501                                 _("Found '..' in authz rule path"))),
502             _("Non-canonical path '%s' in authz rule [%s]"),
503             path, section);
504
505       /* Make space for the current segment. */
506       ++nseg;
507       svn_membuf__resize(&cb->rule_path_buffer, nseg * sizeof(*segment));
508       segment = cb->rule_path_buffer.data;
509       segment += (nseg - 1);
510
511       if (!glob)
512         {
513           /* Trivial case: this is not a glob rule, so every segment
514              is a literal match. */
515           segment->kind = authz_rule_literal;
516           intern_pattern(cb, &segment->pattern, start, pattern_len);
517           continue;
518         }
519
520       /* Copy the segment into the temporary buffer. */
521       svn_stringbuf_setempty(pattern);
522       svn_stringbuf_appendbytes(pattern, start, pattern_len);
523
524       if (0 == apr_fnmatch_test(pattern->data))
525         {
526           /* It's a literal match after all. */
527           segment->kind = authz_rule_literal;
528           unescape_in_place(pattern);
529           intern_pattern(cb, &segment->pattern, pattern->data, pattern->len);
530           continue;
531         }
532
533       if (*pattern->data == '*')
534         {
535           if (pattern->len == 1
536               || (pattern->len == 2 && pattern->data[1] == '*'))
537             {
538               /* Process * and **, applying normalization as per
539                  https://wiki.apache.org/subversion/AuthzImprovements. */
540
541               authz_rule_segment_t *const prev =
542                 (nseg > 1 ? segment - 1 : NULL);
543
544               if (pattern_len == 1)
545                 {
546                   /* This is a *. Replace **|* with *|**. */
547                   if (prev && prev->kind == authz_rule_any_recursive)
548                     {
549                       prev->kind = authz_rule_any_segment;
550                       segment->kind = authz_rule_any_recursive;
551                     }
552                   else
553                     segment->kind = authz_rule_any_segment;
554                 }
555               else
556                 {
557                   /* This is a **. Replace **|** with a single **. */
558                   if (prev && prev->kind == authz_rule_any_recursive)
559                     {
560                       /* Simply drop the redundant new segment. */
561                       --nseg;
562                       continue;
563                     }
564                   else
565                     segment->kind = authz_rule_any_recursive;
566                 }
567
568               segment->pattern.data = interned_empty_string;
569               segment->pattern.len = 0;
570               continue;
571             }
572
573           /* Maybe it's a suffix match? */
574           if (0 == apr_fnmatch_test(pattern->data + 1))
575             {
576               svn_stringbuf_leftchop(pattern, 1);
577               segment->kind = authz_rule_suffix;
578               unescape_in_place(pattern);
579               svn_authz__reverse_string(pattern->data, pattern->len);
580               intern_pattern(cb, &segment->pattern,
581                              pattern->data, pattern->len);
582               continue;
583             }
584         }
585
586       if (pattern->data[pattern->len - 1] == '*')
587         {
588           /* Might be a prefix match. Note that because of the
589              previous test, we already know that the pattern is longer
590              than one character. */
591           if (pattern->data[pattern->len - 2] != '\\')
592             {
593               /* OK, the * wasn't  escaped. Chop off the wildcard. */
594               svn_stringbuf_chop(pattern, 1);
595               if (0 == apr_fnmatch_test(pattern->data))
596                 {
597                   segment->kind = authz_rule_prefix;
598                   unescape_in_place(pattern);
599                   intern_pattern(cb, &segment->pattern,
600                                  pattern->data, pattern->len);
601                   continue;
602                 }
603
604               /* Restore the wildcard since it was not a prefix match. */
605               svn_stringbuf_appendbyte(pattern, '*');
606             }
607         }
608
609       /* It's a generic fnmatch pattern. */
610       segment->kind = authz_rule_fnmatch;
611       intern_pattern(cb, &segment->pattern, pattern->data, pattern->len);
612     }
613
614   SVN_ERR_ASSERT(nseg > 0);
615
616   /* Copy the temporary segments array into the result pool. */
617   {
618     const apr_size_t path_size = nseg * sizeof(*segment);
619     SVN_ERR_ASSERT(path_size <= cb->rule_path_buffer.size);
620
621     rule->len = nseg;
622     rule->path = apr_palloc(cb->authz->pool, path_size);
623     memcpy(rule->path, cb->rule_path_buffer.data, path_size);
624   }
625
626   return SVN_NO_ERROR;
627 }
628
629
630 /* Check that the parsed RULE is unique within the authz file.
631    With the introduction of wildcards, just looking at the SECTION
632    names is not sufficient to determine uniqueness.
633    Use pools and buffers from CB to do the obvious thing. */
634 static svn_error_t *
635 check_unique_rule(ctor_baton_t *cb,
636                   const authz_rule_t *rule,
637                   const char *section)
638 {
639   svn_stringbuf_t *const buf = cb->rule_string_buffer;
640   const char *exists;
641   int i;
642
643   /* Construct the key for this rule */
644   svn_stringbuf_setempty(buf);
645   svn_stringbuf_appendcstr(buf, rule->repos);
646   svn_stringbuf_appendbyte(buf, '\n');
647
648   for (i = 0; i < rule->len; ++i)
649     {
650       authz_rule_segment_t *const seg = &rule->path[i];
651       svn_stringbuf_appendbyte(buf, '@' + seg->kind);
652       svn_stringbuf_appendbytes(buf, seg->pattern.data, seg->pattern.len);
653       svn_stringbuf_appendbyte(buf, '\n');
654     }
655
656   /* Check if the section exists. */
657   exists = apr_hash_get(cb->parsed_rules, buf->data, buf->len);
658   if (exists)
659     return svn_error_createf(
660         SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
661         _("Section [%s] describes the same rule as section [%s]"),
662         section, exists);
663
664   /* Insert the rule into the known rules set. */
665   apr_hash_set(cb->parsed_rules,
666                apr_pstrmemdup(cb->parser_pool, buf->data, buf->len),
667                buf->len,
668                apr_pstrdup(cb->parser_pool, section));
669
670   return SVN_NO_ERROR;
671 }
672
673
674 /* Constructor callback: Starts a rule or [aliases] section. */
675 static svn_error_t *
676 rules_open_section(void *baton, svn_stringbuf_t *section)
677 {
678   ctor_baton_t *const cb = baton;
679   const char *rule = section->data;
680   apr_size_t rule_len = section->len;
681   svn_boolean_t glob;
682   const char *endp;
683   parsed_acl_t acl;
684
685   SVN_ERR(check_open_section(cb, section));
686
687   /* Parse rule property tokens. */
688   if (*rule != ':')
689     glob = FALSE;
690   else
691     {
692       /* This must be a wildcard rule. */
693       apr_size_t token_len;
694
695       ++rule; --rule_len;
696       endp = memchr(rule, ':', rule_len);
697       if (!endp)
698         return svn_error_createf(
699             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
700             _("Empty repository name in authz rule [%s]"),
701             section->data);
702
703       /* Note: the size of glob_rule_token includes the NUL terminator. */
704       token_len = endp - rule;
705       if (token_len != sizeof(glob_rule_token) - 1
706           || memcmp(rule, glob_rule_token, token_len))
707         return svn_error_createf(
708             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
709             _("Invalid type token '%s' in authz rule [%s]"),
710             apr_pstrmemdup(cb->parser_pool, rule, token_len),
711             section->data);
712
713       glob = TRUE;
714       rule = endp + 1;
715       rule_len -= token_len + 1;
716     }
717
718   /* Parse the repository name. */
719   endp = (*rule == '/' ? NULL : memchr(rule, ':', rule_len));
720   if (!endp)
721     acl.acl.rule.repos = interned_empty_string;
722   else
723     {
724       const apr_size_t repos_len = endp - rule;
725
726       /* The rule contains a repository name. */
727       if (0 == repos_len)
728         return svn_error_createf(
729             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
730             _("Empty repository name in authz rule [%s]"),
731             section->data);
732
733       acl.acl.rule.repos = intern_string(cb, rule, repos_len);
734       rule = endp + 1;
735       rule_len -= repos_len + 1;
736     }
737
738   /* Parse the actual rule. */
739   if (*rule == '/')
740     {
741       SVN_ERR(parse_rule_path(&acl.acl.rule, cb, glob, rule, rule_len,
742                               section->data));
743       SVN_ERR(check_unique_rule(cb, &acl.acl.rule, section->data));
744     }
745   else if (0 == strcmp(section->data, aliases_section))
746     {
747       cb->in_aliases = TRUE;
748       return SVN_NO_ERROR;
749     }
750   else
751     {
752       /* This must be the [groups] section. */
753       return groups_open_section(cb, section);
754     }
755
756   acl.acl.sequence_number = cb->parsed_acls->nelts;
757   acl.acl.anon_access = authz_access_none;
758   acl.acl.has_anon_access = FALSE;
759   acl.acl.authn_access = authz_access_none;
760   acl.acl.has_authn_access = FALSE;
761   acl.acl.user_access = NULL;
762
763   acl.aces = svn_hash__make(cb->parser_pool);
764   acl.alias_aces = svn_hash__make(cb->parser_pool);
765
766   cb->current_acl = &APR_ARRAY_PUSH(cb->parsed_acls, parsed_acl_t);
767   *cb->current_acl = acl;
768   return SVN_NO_ERROR;
769 }
770
771
772 /* Parses an alias declaration. The definition (username) of the
773    alias will always be interned. */
774 static svn_error_t *
775 add_alias_definition(ctor_baton_t *cb,
776                      svn_stringbuf_t *option, svn_stringbuf_t *value)
777 {
778   const char *alias;
779   apr_size_t alias_len;
780   const char *user;
781
782   if (strchr("@$&*~", *option->data))
783     return svn_error_createf(
784         SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
785         _("Alias name '%s' may not begin with '%c'"),
786         option->data, *option->data);
787
788   /* Decorate the name to make lookups consistent. */
789   alias = apr_pstrcat(cb->parser_pool, "&", option->data, SVN_VA_NULL);
790   alias_len = option->len + 1;
791   if (apr_hash_get(cb->parsed_aliases, alias, alias_len))
792     return svn_error_createf(
793         SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
794         _("Can't override definition of alias '%s'"),
795         alias);
796
797   user = intern_string(cb, value->data, value->len);
798   apr_hash_set(cb->parsed_aliases, alias, alias_len, user);
799
800   /* Prepare the global rights struct for this user. */
801   prepare_global_rights(cb, user);
802   return SVN_NO_ERROR;
803 }
804
805 /* Parses an access entry. Groups and users in access entry names will
806    always be interned, aliases will never be. */
807 static svn_error_t *
808 add_access_entry(ctor_baton_t *cb, svn_stringbuf_t *section,
809                  svn_stringbuf_t *option, svn_stringbuf_t *value)
810 {
811   parsed_acl_t *const acl = cb->current_acl;
812   const char *name = option->data;
813   apr_size_t name_len = option->len;
814   const svn_boolean_t inverted = (*name == '~');
815   svn_boolean_t anonymous = FALSE;
816   svn_boolean_t authenticated = FALSE;
817   authz_access_t access = authz_access_none;
818   authz_ace_t *ace;
819   int i;
820
821   SVN_ERR_ASSERT(acl != NULL);
822
823   if (inverted)
824     {
825       ++name;
826       --name_len;
827     }
828
829   /* Determine the access entry type. */
830   switch (*name)
831     {
832     case '~':
833       return svn_error_createf(
834           SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
835           _("Access entry '%s' has more than one inversion;"
836             " double negatives are not permitted"),
837           option->data);
838       break;
839
840     case '*':
841       if (name_len != 1)
842         return svn_error_createf(
843             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
844             _("Access entry '%s' is not valid;"
845               " it must be a single '*'"),
846             option->data);
847
848       if (inverted)
849         return svn_error_createf(
850             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
851             _("Access entry '~*' will never match"));
852
853       anonymous = TRUE;
854       authenticated = TRUE;
855       break;
856
857     case '$':
858       if (0 == strcmp(name, anon_access_token))
859         {
860           if (inverted)
861             authenticated = TRUE;
862           else
863             anonymous = TRUE;
864         }
865       else if (0 == strcmp(name, authn_access_token))
866         {
867           if (inverted)
868             anonymous = TRUE;
869           else
870             authenticated = TRUE;
871         }
872       else
873         return svn_error_createf(
874             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
875             _("Access entry token '%s' is not valid;"
876               " should be '%s' or '%s'"),
877             option->data, anon_access_token, authn_access_token);
878       break;
879
880     default:
881       /* A username, group name or alias. */;
882     }
883
884   /* Parse the access rights. */
885   for (i = 0; i < value->len; ++i)
886     {
887       const char access_code = value->data[i];
888       switch (access_code)
889         {
890         case 'r':
891           access |= authz_access_read_flag;
892           break;
893
894         case 'w':
895           access |= authz_access_write_flag;
896           break;
897
898         default:
899           if (!svn_ctype_isspace(access_code))
900             return svn_error_createf(
901                 SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
902                 _("The access mode '%c' in access entry '%s'"
903                   " of rule [%s] is not valid"),
904                 access_code, option->data, section->data);
905       }
906     }
907
908   /* We do not support write-only access. */
909   if ((access & authz_access_write_flag) && !(access & authz_access_read_flag))
910     return svn_error_createf(
911         SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
912         _("Write-only access entry '%s' of rule [%s] is not valid"),
913         option->data, section->data);
914
915   /* Update the parsed ACL with this access entry. */
916   if (anonymous || authenticated)
917     {
918       if (anonymous)
919         {
920           acl->acl.has_anon_access = TRUE;
921           acl->acl.anon_access |= access;
922         }
923       if (authenticated)
924         {
925           acl->acl.has_authn_access = TRUE;
926           acl->acl.authn_access |= access;
927         }
928     }
929   else
930     {
931       /* The inversion tag must be part of the key in the hash
932          table, otherwise we can't tell regular and inverted
933          entries appart. */
934       const char *key = (inverted ? name - 1 : name);
935       const apr_size_t key_len = (inverted ? name_len + 1 : name_len);
936       const svn_boolean_t aliased = (*name == '&');
937       apr_hash_t *aces = (aliased ? acl->alias_aces : acl->aces);
938
939       ace = apr_hash_get(aces, key, key_len);
940       if (ace)
941         ace->access |= access;
942       else
943         {
944           ace = apr_palloc(cb->parser_pool, sizeof(*ace));
945           ace->name = (aliased
946                        ? apr_pstrmemdup(cb->parser_pool, name, name_len)
947                        : intern_string(cb, name, name_len));
948           ace->members = NULL;
949           ace->inverted = inverted;
950           ace->access = access;
951
952           key = (inverted
953                  ? apr_pstrmemdup(cb->parser_pool, key, key_len)
954                  : ace->name);
955           apr_hash_set(aces, key, key_len, ace);
956
957           /* Prepare the global rights struct for this user. */
958           if (!aliased && *ace->name != '@')
959             prepare_global_rights(cb, ace->name);
960         }
961     }
962
963   return SVN_NO_ERROR;
964 }
965
966 /* Constructor callback: Parse a rule, alias or group delcaration. */
967 static svn_error_t *
968 rules_add_value(void *baton, svn_stringbuf_t *section,
969                 svn_stringbuf_t *option, svn_stringbuf_t *value)
970 {
971   ctor_baton_t *const cb = baton;
972
973   if (cb->in_groups)
974     return groups_add_value(baton, section, option, value);
975
976   if (cb->in_aliases)
977     return add_alias_definition(cb, option, value);
978
979   return add_access_entry(cb, section, option, value);
980 }
981
982
983 /* Constructor callback: Close a section. */
984 static svn_error_t *
985 close_section(void *baton, svn_stringbuf_t *section)
986 {
987   ctor_baton_t *const cb = baton;
988
989   SVN_ERR_ASSERT(0 == strcmp(cb->section, section->data));
990   cb->section = NULL;
991   cb->current_acl = NULL;
992   cb->in_groups = FALSE;
993   cb->in_aliases = FALSE;
994   return SVN_NO_ERROR;
995 }
996
997
998 /* Add a user to GROUP.
999    GROUP is never internalized, but USER always is. */
1000 static void
1001 add_to_group(ctor_baton_t *cb, const char *group, const char *user)
1002 {
1003   apr_hash_t *members = svn_hash_gets(cb->expanded_groups, group);
1004   if (!members)
1005     {
1006       group = intern_string(cb, group, -1);
1007       members = svn_hash__make(cb->authz->pool);
1008       svn_hash_sets(cb->expanded_groups, group, members);
1009     }
1010   svn_hash_sets(members, user, interned_empty_string);
1011 }
1012
1013
1014 /* Hash iterator for expanding group definitions.
1015    WARNING: This function is recursive! */
1016 static svn_error_t *
1017 expand_group_callback(void *baton,
1018                       const void *key,
1019                       apr_ssize_t klen,
1020                       void *value,
1021                       apr_pool_t *scratch_pool)
1022 {
1023   ctor_baton_t *const cb = baton;
1024   const char *const group = key;
1025   apr_array_header_t *members = value;
1026
1027   int i;
1028   for (i = 0; i < members->nelts; ++i)
1029     {
1030       const char *member = APR_ARRAY_IDX(members, i, const char*);
1031       if (0 == strcmp(member, group))
1032             return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1033                                      _("Recursive definition of group '%s'"),
1034                                      group);
1035
1036       if (*member == '&')
1037         {
1038           /* Add expanded alias to the group.
1039              N.B.: the user name is already internalized. */
1040           const char *user = svn_hash_gets(cb->parsed_aliases, member);
1041           if (!user)
1042             return svn_error_createf(
1043                 SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1044                 _("Alias '%s' was never defined"),
1045                 member);
1046
1047           add_to_group(cb, group, user);
1048         }
1049       else if (*member != '@')
1050         {
1051           /* Add the member to the group. */
1052           const char *user = intern_string(cb, member, -1);
1053           add_to_group(cb, group, user);
1054
1055           /* Prepare the global rights struct for this user. */
1056           prepare_global_rights(cb, user);
1057         }
1058       else
1059         {
1060           /* Recursively expand the group membership */
1061           members = svn_hash_gets(cb->parsed_groups, member);
1062           if (!members)
1063             return svn_error_createf(
1064                 SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1065                 _("Undefined group '%s'"),
1066                 member);
1067           SVN_ERR(expand_group_callback(cb, key, klen,
1068                                         members, scratch_pool));
1069         }
1070     }
1071   return SVN_NO_ERROR;
1072 }
1073
1074
1075 /* Hash iteration baton for merge_alias_ace. */
1076 typedef struct merge_alias_baton_t
1077 {
1078   apr_hash_t *aces;
1079   ctor_baton_t *cb;
1080 } merge_alias_baton_t;
1081
1082 /* Hash iterator for expanding and mergina alias-based ACEs
1083    into the user/group-based ACEs. */
1084 static svn_error_t *
1085 merge_alias_ace(void *baton,
1086                 const void *key,
1087                 apr_ssize_t klen,
1088                 void *value,
1089                 apr_pool_t *scratch_pool)
1090 {
1091   merge_alias_baton_t *const mab = baton;
1092   authz_ace_t *aliased_ace = value;
1093   const char *alias = aliased_ace->name;
1094   const char *unaliased_key;
1095   const char *user;
1096   authz_ace_t *ace;
1097
1098   user = svn_hash_gets(mab->cb->parsed_aliases, alias);
1099   if (!user)
1100     return svn_error_createf(
1101         SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1102         _("Alias '%s' was never defined"),
1103         alias);
1104
1105   /* N.B.: The user name is always internalized,
1106      but the inverted key may not be. */
1107   if (!aliased_ace->inverted)
1108     unaliased_key = user;
1109   else
1110     {
1111       unaliased_key = apr_pstrcat(mab->cb->parser_pool,
1112                                   "~", user, SVN_VA_NULL);
1113       unaliased_key = intern_string(mab->cb, unaliased_key, -1);
1114     }
1115
1116   ace = svn_hash_gets(mab->aces, unaliased_key);
1117   if (!ace)
1118     {
1119       aliased_ace->name = user;
1120       svn_hash_sets(mab->aces, unaliased_key, aliased_ace);
1121     }
1122   else
1123     {
1124       SVN_ERR_ASSERT(!ace->inverted == !aliased_ace->inverted);
1125       ace->access |= aliased_ace->access;
1126     }
1127
1128   return SVN_NO_ERROR;
1129 }
1130
1131
1132 /* Hash iteration baton for array_insert_ace. */
1133 typedef struct insert_ace_baton_t
1134 {
1135   apr_array_header_t *ace_array;
1136   ctor_baton_t *cb;
1137 } insert_ace_baton_t;
1138
1139 /* Hash iterator, inserts an ACE into the ACLs array. */
1140 static svn_error_t *
1141 array_insert_ace(void *baton,
1142                  const void *key,
1143                  apr_ssize_t klen,
1144                  void *value,
1145                  apr_pool_t *scratch_pool)
1146 {
1147   insert_ace_baton_t *iab = baton;
1148   authz_ace_t *ace = value;
1149
1150   /* Add group membership info to the ACE. */
1151   if (*ace->name == '@')
1152     {
1153       SVN_ERR_ASSERT(ace->members == NULL);
1154       ace->members = svn_hash_gets(iab->cb->expanded_groups, ace->name);
1155       if (!ace->members)
1156         return svn_error_createf(
1157             SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1158             _("Access entry refers to undefined group '%s'"),
1159             ace->name);
1160     }
1161
1162   APR_ARRAY_PUSH(iab->ace_array, authz_ace_t) = *ace;
1163   return SVN_NO_ERROR;
1164 }
1165
1166
1167 /* Update accumulated RIGHTS from ACCESS. */
1168 static void
1169 update_rights(authz_rights_t *rights,
1170               authz_access_t access)
1171 {
1172   rights->min_access &= access;
1173   rights->max_access |= access;
1174 }
1175
1176
1177 /* Update a global RIGHTS based on REPOS and ACCESS. */
1178 static void
1179 update_global_rights(authz_global_rights_t *gr,
1180                      const char *repos,
1181                      authz_access_t access)
1182 {
1183   update_rights(&gr->all_repos_rights, access);
1184   if (0 == strcmp(repos, AUTHZ_ANY_REPOSITORY))
1185     update_rights(&gr->any_repos_rights, access);
1186   else
1187     {
1188       authz_rights_t *rights = svn_hash_gets(gr->per_repos_rights, repos);
1189       if (rights)
1190         update_rights(rights, access);
1191       else
1192         {
1193           rights = apr_palloc(apr_hash_pool_get(gr->per_repos_rights),
1194                               sizeof(*rights));
1195           init_rights(rights);
1196           update_rights(rights, access);
1197           svn_hash_sets(gr->per_repos_rights, repos, rights);
1198         }
1199     }
1200 }
1201
1202
1203 /* Hash iterator to update global per-user rights from an ACL. */
1204 static svn_error_t *
1205 update_user_rights(void *baton,
1206                    const void *key,
1207                    apr_ssize_t klen,
1208                    void *value,
1209                    apr_pool_t *scratch_pool)
1210 {
1211   const authz_acl_t *const acl = baton;
1212   const char *const user = key;
1213   authz_global_rights_t *const gr = value;
1214   authz_access_t access;
1215   svn_boolean_t has_access =
1216     svn_authz__get_acl_access(&access, acl, user, acl->rule.repos);
1217
1218   if (has_access)
1219     update_global_rights(gr, acl->rule.repos, access);
1220   return SVN_NO_ERROR;
1221 }
1222
1223
1224 /* List iterator, expands/merges a parsed ACL into its final form and
1225    appends it to the authz info's ACL array. */
1226 static svn_error_t *
1227 expand_acl_callback(void *baton,
1228                     void *item,
1229                     apr_pool_t *scratch_pool)
1230 {
1231   ctor_baton_t *const cb = baton;
1232   parsed_acl_t *const pacl = item;
1233   authz_acl_t *const acl = &pacl->acl;
1234
1235   /* Expand and merge the aliased ACEs. */
1236   if (apr_hash_count(pacl->alias_aces))
1237     {
1238       merge_alias_baton_t mab;
1239       mab.aces = pacl->aces;
1240       mab.cb = cb;
1241       SVN_ERR(svn_iter_apr_hash(NULL, pacl->alias_aces,
1242                                 merge_alias_ace, &mab, scratch_pool));
1243     }
1244
1245   /* Make an array from the merged hashes. */
1246   acl->user_access =
1247     apr_array_make(cb->authz->pool, apr_hash_count(pacl->aces),
1248                    sizeof(authz_ace_t));
1249   {
1250     insert_ace_baton_t iab;
1251     iab.ace_array = acl->user_access;
1252     iab.cb = cb;
1253     SVN_ERR(svn_iter_apr_hash(NULL, pacl->aces,
1254                               array_insert_ace, &iab, scratch_pool));
1255   }
1256
1257   /* Store the completed ACL into authz. */
1258   APR_ARRAY_PUSH(cb->authz->acls, authz_acl_t) = *acl;
1259
1260   /* Update global access rights for this ACL. */
1261   if (acl->has_anon_access)
1262     {
1263       cb->authz->has_anon_rights = TRUE;
1264       update_global_rights(&cb->authz->anon_rights,
1265                            acl->rule.repos, acl->anon_access);
1266     }
1267   if (acl->has_authn_access)
1268     {
1269       cb->authz->has_authn_rights = TRUE;
1270       update_global_rights(&cb->authz->authn_rights,
1271                            acl->rule.repos, acl->authn_access);
1272     }
1273   SVN_ERR(svn_iter_apr_hash(NULL, cb->authz->user_rights,
1274                             update_user_rights, acl, scratch_pool));
1275   return SVN_NO_ERROR;
1276 }
1277
1278
1279 /* Compare two ACLs in rule lexical order, then repository order, then
1280    order of definition. This ensures that our default ACL is always
1281    first in the sorted array. */
1282 static int
1283 compare_parsed_acls(const void *va, const void *vb)
1284 {
1285   const parsed_acl_t *const a = va;
1286   const parsed_acl_t *const b = vb;
1287
1288   int cmp = svn_authz__compare_rules(&a->acl.rule, &b->acl.rule);
1289   if (cmp == 0)
1290     cmp = a->acl.sequence_number - b->acl.sequence_number;
1291   return cmp;
1292 }
1293
1294
1295 svn_error_t *
1296 svn_authz__parse(authz_full_t **authz,
1297                  svn_stream_t *rules,
1298                  svn_stream_t *groups,
1299                  apr_pool_t *result_pool,
1300                  apr_pool_t *scratch_pool)
1301 {
1302   ctor_baton_t *const cb = create_ctor_baton(result_pool, scratch_pool);
1303
1304   /*
1305    * Pass 1: Parse the authz file.
1306    */
1307   SVN_ERR(svn_config__parse_stream(rules,
1308                                    svn_config__constructor_create(
1309                                        rules_open_section,
1310                                        close_section,
1311                                        rules_add_value,
1312                                        cb->parser_pool),
1313                                    cb, cb->parser_pool));
1314
1315   /*
1316    * Pass 1.6487: Parse the global groups file.
1317    */
1318   if (groups)
1319     {
1320       /* Check that the authz file did not contain any groups. */
1321       if (0 != apr_hash_count(cb->parsed_groups))
1322           return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL,
1323                                   ("Authz file cannot contain any groups"
1324                                    " when global groups are being used."));
1325
1326       apr_hash_clear(cb->sections);
1327       cb->parsing_groups = TRUE;
1328       SVN_ERR(svn_config__parse_stream(groups,
1329                                        svn_config__constructor_create(
1330                                            groups_open_section,
1331                                            close_section,
1332                                            groups_add_value,
1333                                            cb->parser_pool),
1334                                        cb, cb->parser_pool));
1335     }
1336
1337   /*
1338    * Pass 2: Expand groups and construct the final svn_authz_t.
1339    */
1340   cb->expanded_groups = svn_hash__make(cb->parser_pool);
1341   SVN_ERR(svn_iter_apr_hash(NULL, cb->parsed_groups,
1342                             expand_group_callback, cb, cb->parser_pool));
1343
1344
1345   /* Sort the parsed ACLs in rule lexical order and pop off the
1346      default global ACL iff an equivalent ACL was defined in the authz
1347      file. */
1348   if (cb->parsed_acls->nelts > 1)
1349     {
1350       parsed_acl_t *defacl;
1351       parsed_acl_t *nxtacl;
1352
1353       svn_sort__array(cb->parsed_acls, compare_parsed_acls);
1354       defacl = &APR_ARRAY_IDX(cb->parsed_acls, 0, parsed_acl_t);
1355       nxtacl = &APR_ARRAY_IDX(cb->parsed_acls, 1, parsed_acl_t);
1356
1357       /* If the first ACL is not our default thingamajig, there's a
1358          bug in our comparator. */
1359       SVN_ERR_ASSERT(
1360           defacl->acl.sequence_number == 0 && defacl->acl.rule.len == 0
1361           && 0 == strcmp(defacl->acl.rule.repos, AUTHZ_ANY_REPOSITORY));
1362
1363       /* Pop the default ACL off the array if another equivalent
1364          exists, after merging the default rights. */
1365       if (0 == svn_authz__compare_rules(&defacl->acl.rule, &nxtacl->acl.rule))
1366         {
1367           nxtacl->acl.has_anon_access = TRUE;
1368           nxtacl->acl.has_authn_access = TRUE;
1369           cb->parsed_acls->elts = (char*)(nxtacl);
1370           --cb->parsed_acls->nelts;
1371         }
1372     }
1373
1374   cb->authz->acls = apr_array_make(cb->authz->pool, cb->parsed_acls->nelts,
1375                                    sizeof(authz_acl_t));
1376   SVN_ERR(svn_iter_apr_array(NULL, cb->parsed_acls,
1377                              expand_acl_callback, cb, cb->parser_pool));
1378
1379   *authz = cb->authz;
1380   apr_pool_destroy(cb->parser_pool);
1381   return SVN_NO_ERROR;
1382 }
1383
1384
1385 void
1386 svn_authz__reverse_string(char *string, apr_size_t len)
1387 {
1388   char *left = string;
1389   char *right = string + len - 1;
1390   for (; left < right; ++left, --right)
1391     {
1392       char c = *left;
1393       *left = *right;
1394       *right = c;
1395     }
1396 }
1397
1398
1399 int
1400 svn_authz__compare_paths(const authz_rule_t *a, const authz_rule_t *b)
1401 {
1402   const int min_len = (a->len > b->len ? b->len : a->len);
1403   int i;
1404
1405   for (i = 0; i < min_len; ++i)
1406     {
1407       int cmp = a->path[i].kind - b->path[i].kind;
1408       if (0 == cmp)
1409         {
1410           const char *const aseg = a->path[i].pattern.data;
1411           const char *const bseg = b->path[i].pattern.data;
1412
1413           /* Exploit the fact that segment patterns are interned. */
1414           if (aseg != bseg)
1415             cmp = strcmp(aseg, bseg);
1416           else
1417             cmp = 0;
1418         }
1419       if (0 != cmp)
1420         return cmp;
1421     }
1422
1423   /* Sort shorter rules first. */
1424   if (a->len != b->len)
1425     return a->len - b->len;
1426
1427   return 0;
1428 }
1429
1430 int
1431 svn_authz__compare_rules(const authz_rule_t *a, const authz_rule_t *b)
1432 {
1433   int diff = svn_authz__compare_paths(a, b);
1434   if (diff)
1435     return diff;
1436
1437   /* Repository names are interned, too. */
1438   if (a->repos != b->repos)
1439     return strcmp(a->repos, b->repos);
1440
1441   return 0;
1442 }