diff options
Diffstat (limited to 'subversion/libsvn_repos')
24 files changed, 5883 insertions, 2183 deletions
diff --git a/subversion/libsvn_repos/authz.c b/subversion/libsvn_repos/authz.c index 20f9231dc22a..668d78dd5993 100644 --- a/subversion/libsvn_repos/authz.c +++ b/subversion/libsvn_repos/authz.c @@ -25,6 +25,7 @@ #include <apr_pools.h> #include <apr_file_io.h> +#include <apr_fnmatch.h> #include "svn_hash.h" #include "svn_pools.h" @@ -34,946 +35,1592 @@ #include "svn_repos.h" #include "svn_config.h" #include "svn_ctype.h" +#include "private/svn_atomic.h" #include "private/svn_fspath.h" #include "private/svn_repos_private.h" +#include "private/svn_sorts_private.h" +#include "private/svn_subr_private.h" #include "repos.h" +#include "authz.h" +#include "config_file.h" -/*** Structures. ***/ +/*** Access rights. ***/ -/* Information for the config enumerators called during authz - lookup. */ -struct authz_lookup_baton { - /* The authz configuration. */ - svn_config_t *config; +/* This structure describes the access rights given to a specific user by + * a path rule (actually the rule set specified for a path). I.e. there is + * one instance of this per path rule. + */ +typedef struct path_access_t +{ + /* Sequence number of the path rule that this struct was derived from. + * If multiple rules apply to the same path (only possible with wildcard + * matching), the one with the highest SEQUENCE_NUMBER wins, i.e. the latest + * one defined in the authz file. + * + * A value of 0 denotes the default rule at the repository root denying + * access to everybody. User-defined path rules start with ID 1. + */ + int sequence_number; - /* The user to authorize. */ - const char *user; + /* Access rights of the respective user as defined by the rule set. */ + authz_access_t rights; +} path_access_t; - /* Explicitly granted rights. */ - svn_repos_authz_access_t allow; - /* Explicitly denied rights. */ - svn_repos_authz_access_t deny; +/* Use this to indicate that no sequence ID has been assigned. + * It will automatically be inferior to (less than) any other sequence ID. */ +#define NO_SEQUENCE_NUMBER (-1) - /* The rights required by the caller of the lookup. */ - svn_repos_authz_access_t required_access; +/* Convenience structure combining the node-local access rights with the + * min and max rights granted within the sub-tree. */ +typedef struct limited_rights_t +{ + /* Access granted to the current user. If the SEQUENCE_NUMBER member is + * NO_SEQUENCE_NUMBER, there has been no specific path rule for this PATH + * but only for some sub-path(s). There is always a rule at the root node. + */ + path_access_t access; - /* The following are used exclusively in recursive lookups. */ + /* Minimal access rights that the user has on this or any other node in + * the sub-tree. This does not take inherited rights into account. */ + authz_access_t min_rights; - /* The path in the repository (an fspath) to authorize. */ - const char *repos_path; - /* repos_path prefixed by the repository name and a colon. */ - const char *qualified_repos_path; + /* Maximal access rights that the user has on this or any other node in + * the sub-tree. This does not take inherited rights into account. */ + authz_access_t max_rights; - /* Whether, at the end of a recursive lookup, access is granted. */ - svn_boolean_t access; -}; +} limited_rights_t; -/* Information for the config enumeration functions called during the - validation process. */ -struct authz_validate_baton { - svn_config_t *config; /* The configuration file being validated. */ - svn_error_t *err; /* The error being thrown out of the - enumerator, if any. */ -}; +/* Return TRUE, if RIGHTS has local rights defined in the ACCESS member. */ +static svn_boolean_t +has_local_rule(const limited_rights_t *rights) +{ + return rights->access.sequence_number != NO_SEQUENCE_NUMBER; +} -/* Currently this structure is just a wrapper around a svn_config_t. - Please update authz_pool if you modify this structure. */ -struct svn_authz_t +/* Aggregate the ACCESS spec of TARGET and RIGHTS into TARGET. I.e. if both + * are specified, pick one in accordance to the precedence rules. */ +static void +combine_access(limited_rights_t *target, + const limited_rights_t *rights) { - svn_config_t *cfg; -}; + /* This implies the check for NO_SEQUENCE_NUMBER, i.e no rights being + * specified. */ + if (target->access.sequence_number < rights->access.sequence_number) + target->access = rights->access; +} +/* Aggregate the min / max access rights of TARGET and RIGHTS into TARGET. */ +static void +combine_right_limits(limited_rights_t *target, + const limited_rights_t *rights) +{ + target->max_rights |= rights->max_rights; + target->min_rights &= rights->min_rights; +} -/*** Checking access. ***/ -/* Determine whether the REQUIRED access is granted given what authz - * to ALLOW or DENY. Return TRUE if the REQUIRED access is - * granted. - * - * Access is granted either when no required access is explicitly - * denied (implicit grant), or when the required access is explicitly - * granted, overriding any denials. +/*** Authz cache access. ***/ + +/* All authz instances currently in use as well as all filtered authz + * instances in use will be cached here. + * Both caches will be instantiated at most once. */ +static svn_object_pool__t *authz_pool = NULL; +static svn_object_pool__t *filtered_pool = NULL; +static svn_atomic_t authz_pool_initialized = FALSE; + +/* Implements svn_atomic__err_init_func_t. */ +static svn_error_t * +synchronized_authz_initialize(void *baton, apr_pool_t *pool) +{ +#if APR_HAS_THREADS + svn_boolean_t multi_threaded = TRUE; +#else + svn_boolean_t multi_threaded = FALSE; +#endif + + SVN_ERR(svn_object_pool__create(&authz_pool, multi_threaded, pool)); + SVN_ERR(svn_object_pool__create(&filtered_pool, multi_threaded, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_authz_initialize(apr_pool_t *pool) +{ + /* Protect against multiple calls. */ + return svn_error_trace(svn_atomic__init_once(&authz_pool_initialized, + synchronized_authz_initialize, + NULL, pool)); +} + +/* Return a combination of AUTHZ_KEY and GROUPS_KEY, allocated in RESULT_POOL. + * GROUPS_KEY may be NULL. This is the key for the AUTHZ_POOL. */ -static svn_boolean_t -authz_access_is_granted(svn_repos_authz_access_t allow, - svn_repos_authz_access_t deny, - svn_repos_authz_access_t required) +static svn_membuf_t * +construct_authz_key(const svn_checksum_t *authz_key, + const svn_checksum_t *groups_key, + apr_pool_t *result_pool) { - svn_repos_authz_access_t stripped_req = - required & (svn_authz_read | svn_authz_write); + svn_membuf_t *result = apr_pcalloc(result_pool, sizeof(*result)); + if (groups_key) + { + apr_size_t authz_size = svn_checksum_size(authz_key); + apr_size_t groups_size = svn_checksum_size(groups_key); - if ((deny & required) == svn_authz_none) - return TRUE; - else if ((allow & required) == stripped_req) - return TRUE; + svn_membuf__create(result, authz_size + groups_size, result_pool); + result->size = authz_size + groups_size; /* exact length is required! */ + + memcpy(result->data, authz_key->digest, authz_size); + memcpy((char *)result->data + authz_size, + groups_key->digest, groups_size); + } else - return FALSE; + { + apr_size_t size = svn_checksum_size(authz_key); + svn_membuf__create(result, size, result_pool); + result->size = size; /* exact length is required! */ + memcpy(result->data, authz_key->digest, size); + } + + return result; } +/* Return a combination of REPOS_NAME, USER and AUTHZ_ID, allocated in + * RESULT_POOL. USER may be NULL. This is the key for the FILTERED_POOL. + */ +static svn_membuf_t * +construct_filtered_key(const char *repos_name, + const char *user, + const svn_membuf_t *authz_id, + apr_pool_t *result_pool) +{ + svn_membuf_t *result = apr_pcalloc(result_pool, sizeof(*result)); + size_t repos_len = strlen(repos_name); + size_t user_len = user ? strlen(user) : 1; + const char *nullable_user = user ? user : "\0"; + size_t size = authz_id->size + repos_len + 1 + user_len + 1; + + svn_membuf__create(result, size, result_pool); + result->size = size; + + memcpy(result->data, repos_name, repos_len + 1); + size = repos_len + 1; + memcpy((char *)result->data + size, nullable_user, user_len + 1); + size += user_len + 1; + memcpy((char *)result->data + size, authz_id->data, authz_id->size); + + return result; +} -/* Decide whether the REQUIRED access has been conclusively - * determined. Return TRUE if the given ALLOW/DENY authz are - * conclusive regarding the REQUIRED authz. - * - * Conclusive determination occurs when any of the REQUIRED authz are - * granted or denied by ALLOW/DENY. + +/*** Constructing the prefix tree. ***/ + +/* Since prefix arrays may have more than one hit, we need to link them + * for fast lookup. */ +typedef struct sorted_pattern_t +{ + /* The filtered tree node carrying the prefix. */ + struct node_t *node; + + /* Entry that is a prefix to this one or NULL. */ + struct sorted_pattern_t *next; +} sorted_pattern_t; + +/* Substructure of node_t. It contains all sub-node that use patterns + * in the next segment level. We keep it separate to save a bit of memory + * and to be able to check for pattern presence in a single operation. */ -static svn_boolean_t -authz_access_is_determined(svn_repos_authz_access_t allow, - svn_repos_authz_access_t deny, - svn_repos_authz_access_t required) +typedef struct node_pattern_t { - if ((deny & required) || (allow & required)) - return TRUE; + /* If not NULL, this represents the "*" follow-segment. */ + struct node_t *any; + + /* If not NULL, this represents the "**" follow-segment. */ + struct node_t *any_var; + + /* If not NULL, the segments of all sorted_pattern_t in this array are the + * prefix part of "prefix*" patterns. Sorted by segment prefix. */ + apr_array_header_t *prefixes; + + /* If not NULL, the segments of all sorted_pattern_t in this array are the + * reversed suffix part of "*suffix" patterns. Sorted by reversed + * segment suffix. */ + apr_array_header_t *suffixes; + + /* If not NULL, the segments of all sorted_pattern_t in this array contain + * wildcards and don't fit into any of the above categories. + * The NEXT members of the elements will not be used. */ + apr_array_header_t *complex; + + /* This node itself is a "**" segment and must therefore itself be added + * to the matching node list for the next level. */ + svn_boolean_t repeat; +} node_pattern_t; + +/* The pattern tree. All relevant path rules are being folded into this + * prefix tree, with a single, whole segment stored at each node. The whole + * tree applies to a single user only. + */ +typedef struct node_t +{ + /* The segment as specified in the path rule. During the lookup tree walk, + * this will compared to the respective segment of the path to check. */ + svn_string_t segment; + + /* Immediate access rights granted by rules on this node and the min / + * max rights on any path in this sub-tree. */ + limited_rights_t rights; + + /* Map of sub-segment(const char *) to respective node (node_t) for all + * sub-segments that have rules on themselves or their respective subtrees. + * NULL, if there are no rules for sub-paths relevant to the user. */ + apr_hash_t *sub_nodes; + + /* If not NULL, this contains the pattern-based segment sub-nodes. */ + node_pattern_t *pattern_sub_nodes; +} node_t; + +/* Create a new tree node for SEGMENT. + Note: SEGMENT->pattern is always interned and therefore does not + have to be copied into the result pool. */ +static node_t * +create_node(authz_rule_segment_t *segment, + apr_pool_t *result_pool) +{ + node_t *result = apr_pcalloc(result_pool, sizeof(*result)); + if (segment) + result->segment = segment->pattern; else - return FALSE; + { + result->segment.data = ""; + result->segment.len = 0; + } + result->rights.access.sequence_number = NO_SEQUENCE_NUMBER; + return result; } -/* Return TRUE is USER equals ALIAS. The alias definitions are in the - "aliases" sections of CFG. Use POOL for temporary allocations during - the lookup. */ -static svn_boolean_t -authz_alias_is_user(svn_config_t *cfg, - const char *alias, - const char *user, - apr_pool_t *pool) +/* Auto-create a node in *NODE, make it apply to SEGMENT and return it. */ +static node_t * +ensure_node(node_t **node, + authz_rule_segment_t *segment, + apr_pool_t *result_pool) { - const char *value; + if (!*node) + *node = create_node(segment, result_pool); - svn_config_get(cfg, &value, "aliases", alias, NULL); - if (!value) - return FALSE; + return *node; +} - if (strcmp(value, user) == 0) - return TRUE; +/* compare_func comparing segment names. It takes a sorted_pattern_t* as + * VOID_LHS and a const authz_rule_segment_t * as VOID_RHS. + */ +static int +compare_node_rule_segment(const void *void_lhs, + const void *void_rhs) +{ + const sorted_pattern_t *element = void_lhs; + const authz_rule_segment_t *segment = void_rhs; - return FALSE; + return strcmp(element->node->segment.data, segment->pattern.data); } +/* compare_func comparing segment names. It takes a sorted_pattern_t* as + * VOID_LHS and a const char * as VOID_RHS. + */ +static int +compare_node_path_segment(const void *void_lhs, + const void *void_rhs) +{ + const sorted_pattern_t *element = void_lhs; + const char *segment = void_rhs; + + return strcmp(element->node->segment.data, segment); +} -/* Return TRUE if USER is in GROUP. The group definitions are in the - "groups" section of CFG. Use POOL for temporary allocations during - the lookup. */ -static svn_boolean_t -authz_group_contains_user(svn_config_t *cfg, - const char *group, - const char *user, - apr_pool_t *pool) +/* Make sure a node_t* for SEGMENT exists in *ARRAY and return it. + * Auto-create either if they don't exist. Entries in *ARRAY are + * sorted by their segment strings. + */ +static node_t * +ensure_node_in_array(apr_array_header_t **array, + authz_rule_segment_t *segment, + apr_pool_t *result_pool) { - const char *value; - apr_array_header_t *list; - int i; + int idx; + sorted_pattern_t entry; + sorted_pattern_t *entry_ptr; + + /* Auto-create the array. */ + if (!*array) + *array = apr_array_make(result_pool, 4, sizeof(sorted_pattern_t)); + + /* Find the node in ARRAY and the IDX at which it were to be inserted. + * Initialize IDX such that we won't attempt a hinted lookup (likely + * to fail and therefore pure overhead). */ + idx = (*array)->nelts; + entry_ptr = svn_sort__array_lookup(*array, segment, &idx, + compare_node_rule_segment); + if (entry_ptr) + return entry_ptr->node; + + /* There is no such node, yet. + * Create one and insert it into the sorted array. */ + entry.node = create_node(segment, result_pool); + entry.next = NULL; + svn_sort__array_insert(*array, &entry, idx); + + return entry.node; +} - svn_config_get(cfg, &value, "groups", group, NULL); +/* Auto-create the PATTERN_SUB_NODES sub-structure in *NODE and return it. */ +static node_pattern_t * +ensure_pattern_sub_nodes(node_t *node, + apr_pool_t *result_pool) +{ + if (node->pattern_sub_nodes == NULL) + node->pattern_sub_nodes = apr_pcalloc(result_pool, + sizeof(*node->pattern_sub_nodes)); + + return node->pattern_sub_nodes; +} + +/* Combine an ACL rule segment with the corresponding node in our filtered + * data model. */ +typedef struct node_segment_pair_t +{ + authz_rule_segment_t *segment; + node_t *node; +} node_segment_pair_t; + +/* Context object to be used with process_acl. It allows us to re-use + * information from previous insertions. */ +typedef struct construction_context_t +{ + /* Array of node_segment_pair_t. It contains all segments already + * processed of the current insertion together with the respective + * nodes in our filtered tree. Before the next lookup, the tree + * walk for the common prefix can be skipped. */ + apr_array_header_t *path; +} construction_context_t; + +/* Return a new context object allocated in RESULT_POOL. */ +static construction_context_t * +create_construction_context(apr_pool_t *result_pool) +{ + construction_context_t *result = apr_pcalloc(result_pool, sizeof(*result)); + + /* Array will be auto-extended but this initial size will make it rarely + * ever necessary. */ + result->path = apr_array_make(result_pool, 32, sizeof(node_segment_pair_t)); - list = svn_cstring_split(value, ",", TRUE, pool); + return result; +} - for (i = 0; i < list->nelts; i++) +/* Constructor utility: Below NODE, recursively insert sub-nodes for the + * path given as *SEGMENTS of length SEGMENT_COUNT. If matching nodes + * already exist, use those instead of creating new ones. Set the leave + * node's access rights spec to PATH_ACCESS. Update the context info in CTX. + */ +static void +insert_path(construction_context_t *ctx, + node_t *node, + path_access_t *path_access, + int segment_count, + authz_rule_segment_t *segment, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + node_t *sub_node; + node_segment_pair_t *node_segment; + + /* End of path? */ + if (segment_count == 0) { - const char *group_user = APR_ARRAY_IDX(list, i, char *); + /* Set access rights. Note that there might be multiple rules for + * the same path due to non-repo-specific rules vs. repo-specific + * ones. Whichever gets defined last wins. + */ + limited_rights_t rights; + rights.access = *path_access; + rights.max_rights = path_access->rights; + rights.min_rights = path_access->rights; + combine_access(&node->rights, &rights); + return; + } + + /* Any wildcards? They will go into a separate sub-structure. */ + if (segment->kind != authz_rule_literal) + ensure_pattern_sub_nodes(node, result_pool); - /* If the 'user' is a subgroup, recurse into it. */ - if (*group_user == '@') + switch (segment->kind) + { + /* A full wildcard segment? */ + case authz_rule_any_segment: + sub_node = ensure_node(&node->pattern_sub_nodes->any, + segment, result_pool); + break; + + /* One or more full wildcard segments? */ + case authz_rule_any_recursive: + sub_node = ensure_node(&node->pattern_sub_nodes->any_var, + segment, result_pool); + ensure_pattern_sub_nodes(sub_node, result_pool)->repeat = TRUE; + break; + + /* A single wildcard at the end of the segment? */ + case authz_rule_prefix: + sub_node = ensure_node_in_array(&node->pattern_sub_nodes->prefixes, + segment, result_pool); + break; + + /* A single wildcard at the start of segments? */ + case authz_rule_suffix: + sub_node = ensure_node_in_array(&node->pattern_sub_nodes->suffixes, + segment, result_pool); + break; + + /* General pattern? */ + case authz_rule_fnmatch: + sub_node = ensure_node_in_array(&node->pattern_sub_nodes->complex, + segment, result_pool); + break; + + /* Then it must be a literal. */ + default: + SVN_ERR_ASSERT_NO_RETURN(segment->kind == authz_rule_literal); + + if (!node->sub_nodes) { - if (authz_group_contains_user(cfg, &group_user[1], - user, pool)) - return TRUE; + node->sub_nodes = svn_hash__make(result_pool); + sub_node = NULL; } - - /* If the 'user' is an alias, verify it. */ - else if (*group_user == '&') + else { - if (authz_alias_is_user(cfg, &group_user[1], - user, pool)) - return TRUE; + sub_node = svn_hash_gets(node->sub_nodes, segment->pattern.data); } - /* If the user matches, stop. */ - else if (strcmp(user, group_user) == 0) - return TRUE; + /* Auto-insert a sub-node for the current segment. */ + if (!sub_node) + { + sub_node = create_node(segment, result_pool); + apr_hash_set(node->sub_nodes, + sub_node->segment.data, + sub_node->segment.len, + sub_node); + } } - return FALSE; + /* Update context. */ + node_segment = apr_array_push(ctx->path); + node_segment->segment = segment; + node_segment->node = sub_node; + + /* Continue at the sub-node with the next segment. */ + insert_path(ctx, sub_node, path_access, segment_count - 1, segment + 1, + result_pool, scratch_pool); } -/* Determines whether an authz rule applies to the current - * user, given the name part of the rule's name-value pair - * in RULE_MATCH_STRING and the authz_lookup_baton object - * B with the username in question. +/* If the ACL is relevant to the REPOSITORY and user (given as MEMBERSHIPS + * plus ANONYMOUS flag), insert the respective nodes into tree starting + * at ROOT. Use the context info of the previous call in CTX to eliminate + * repeated lookups. Allocate new nodes in RESULT_POOL and use SCRATCH_POOL + * for temporary allocations. */ -static svn_boolean_t -authz_line_applies_to_user(const char *rule_match_string, - struct authz_lookup_baton *b, - apr_pool_t *pool) +static void +process_acl(construction_context_t *ctx, + const authz_acl_t *acl, + node_t *root, + const char *repository, + const char *user, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { - /* If the rule has an inversion, recurse and invert the result. */ - if (rule_match_string[0] == '~') - return !authz_line_applies_to_user(&rule_match_string[1], b, pool); - - /* Check for special tokens. */ - if (strcmp(rule_match_string, "$anonymous") == 0) - return (b->user == NULL); - if (strcmp(rule_match_string, "$authenticated") == 0) - return (b->user != NULL); - - /* Check for a wildcard rule. */ - if (strcmp(rule_match_string, "*") == 0) - return TRUE; + path_access_t path_access; + int i; + node_t *node; - /* If we get here, then the rule is: - * - Not an inversion rule. - * - Not an authz token rule. - * - Not a wildcard rule. - * - * All that's left over is regular user or group specifications. - */ + /* Skip ACLs that don't say anything about the current user + and/or repository. */ + if (!svn_authz__get_acl_access(&path_access.rights, acl, user, repository)) + return; - /* If the session is anonymous, then a user/group - * rule definitely won't match. - */ - if (b->user == NULL) - return FALSE; + /* Insert the rule into the filtered tree. */ + path_access.sequence_number = acl->sequence_number; - /* Process the rule depending on whether it is - * a user, alias or group rule. - */ - if (rule_match_string[0] == '@') - return authz_group_contains_user( - b->config, &rule_match_string[1], b->user, pool); - else if (rule_match_string[0] == '&') - return authz_alias_is_user( - b->config, &rule_match_string[1], b->user, pool); - else - return (strcmp(b->user, rule_match_string) == 0); -} + /* Try to reuse results from previous runs. + * Basically, skip the common prefix. */ + node = root; + for (i = 0; i < ctx->path->nelts; ++i) + { + const node_segment_pair_t *step + = &APR_ARRAY_IDX(ctx->path, i, const node_segment_pair_t); + + /* Exploit the fact that all strings in the authz model are unique / + * internized and can be identified by address alone. */ + if ( !step->node + || i >= acl->rule.len + || step->segment->kind != acl->rule.path[i].kind + || step->segment->pattern.data != acl->rule.path[i].pattern.data) + { + ctx->path->nelts = i; + break; + } + else + { + node = step->node; + } + } + /* Insert the path rule into the filtered tree. */ + insert_path(ctx, node, &path_access, + acl->rule.len - i, acl->rule.path + i, + result_pool, scratch_pool); +} -/* Callback to parse one line of an authz file and update the - * authz_baton accordingly. +/* Forward declaration ... */ +static svn_boolean_t +trim_tree(node_t *node, + int latest_any_var, + apr_pool_t *scratch_pool); + +/* Call trim_tree() with LATEST_ANY_VAR on all elements in the *HASH of + * node_t * and remove empty nodes from. *HASH may be NULL. If all nodes + * could be removed, set *HASH to NULL and return TRUE. Allocate temporary + * data in SCRATCH_POOL. */ static svn_boolean_t -authz_parse_line(const char *name, const char *value, - void *baton, apr_pool_t *pool) +trim_subnode_hash(apr_hash_t **hash, + int latest_any_var, + apr_pool_t *scratch_pool) { - struct authz_lookup_baton *b = baton; + if (*hash) + { + apr_array_header_t *to_remove = apr_array_make(scratch_pool, 0, + sizeof(node_t *)); - /* Stop if the rule doesn't apply to this user. */ - if (!authz_line_applies_to_user(name, b, pool)) - return TRUE; + apr_hash_index_t *hi; + for (hi = apr_hash_first(scratch_pool, *hash); + hi; + hi = apr_hash_next(hi)) + { + node_t *node = apr_hash_this_val(hi); + if (trim_tree(node, latest_any_var, scratch_pool)) + APR_ARRAY_PUSH(to_remove, node_t *) = node; + } - /* Set the access grants for the rule. */ - if (strchr(value, 'r')) - b->allow |= svn_authz_read; - else - b->deny |= svn_authz_read; + /* Are some nodes left? */ + if (to_remove->nelts < apr_hash_count(*hash)) + { + /* Remove empty nodes (if any). */ + int i; + for (i = 0; i < to_remove->nelts; ++i) + { + node_t *node = APR_ARRAY_IDX(to_remove, i, node_t *); + apr_hash_set(*hash, node->segment.data, node->segment.len, + NULL); + } - if (strchr(value, 'w')) - b->allow |= svn_authz_write; - else - b->deny |= svn_authz_write; + return FALSE; + } + + /* No nodes left. A NULL hash is more efficient than an empty one. */ + *hash = NULL; + } return TRUE; } - -/* Return TRUE iff the access rules in SECTION_NAME apply to PATH_SPEC - * (which is a repository name, colon, and repository fspath, such as - * "myrepos:/trunk/foo"). +/* Call trim_tree() with LATEST_ANY_VAR on all elements in the *ARRAY of + * node_t * and remove empty nodes from. *ARRAY may be NULL. If all nodes + * could be removed, set *ARRAY to NULL and return TRUE. Allocate + * temporary data in SCRATCH_POOL. */ static svn_boolean_t -is_applicable_section(const char *path_spec, - const char *section_name) +trim_subnode_array(apr_array_header_t **array, + int latest_any_var, + apr_pool_t *scratch_pool) { - apr_size_t path_spec_len = strlen(path_spec); + if (*array) + { + int i, dest; + for (i = 0, dest = 0; i < (*array)->nelts; ++i) + { + node_t *node = APR_ARRAY_IDX(*array, i, sorted_pattern_t).node; + if (!trim_tree(node, latest_any_var, scratch_pool)) + { + if (i != dest) + APR_ARRAY_IDX(*array, dest, sorted_pattern_t) + = APR_ARRAY_IDX(*array, i, sorted_pattern_t); + ++dest; + } + } - return ((strncmp(path_spec, section_name, path_spec_len) == 0) - && (path_spec[path_spec_len - 1] == '/' - || section_name[path_spec_len] == '/' - || section_name[path_spec_len] == '\0')); -} + /* Are some nodes left? */ + if (dest) + { + /* Trim it to the number of valid entries. */ + (*array)->nelts = dest; + return FALSE; + } + + /* No nodes left. A NULL array is more efficient than an empty one. */ + *array = NULL; + } + return TRUE; +} -/* Callback to parse a section and update the authz_baton if the - * section denies access to the subtree the baton describes. +/* Remove all rules and sub-nodes from NODE that are fully eclipsed by the + * "any-var" rule with sequence number LATEST_ANY_VAR. Return TRUE, if + * there are no rules left in the sub-tree, including NODE. + * Allocate temporary data in SCRATCH_POOL. */ static svn_boolean_t -authz_parse_section(const char *section_name, void *baton, apr_pool_t *pool) +trim_tree(node_t *node, + int latest_any_var, + apr_pool_t *scratch_pool) { - struct authz_lookup_baton *b = baton; - svn_boolean_t conclusive; + svn_boolean_t removed_all = TRUE; - /* Does the section apply to us? */ - if (!is_applicable_section(b->qualified_repos_path, section_name) - && !is_applicable_section(b->repos_path, section_name)) + /* For convenience, we allow NODE to be NULL: */ + if (!node) return TRUE; - /* Work out what this section grants. */ - b->allow = b->deny = 0; - svn_config_enumerate2(b->config, section_name, - authz_parse_line, b, pool); + /* Do we have a later "any_var" rule at this node. */ + if ( node->pattern_sub_nodes + && node->pattern_sub_nodes->any_var + && node->pattern_sub_nodes->any_var->rights.access.sequence_number + > latest_any_var) + { + latest_any_var + = node->pattern_sub_nodes->any_var->rights.access.sequence_number; + } + + /* Is there a local rule at this node that is not eclipsed by any_var? */ + if (has_local_rule(&node->rights)) + { + /* Remove the local rule, if it got eclipsed. + * Note that for the latest any_var node, the sequence number is equal. */ + if (node->rights.access.sequence_number >= latest_any_var) + removed_all = FALSE; + else + node->rights.access.sequence_number = NO_SEQUENCE_NUMBER; + } + + /* Process all sub-nodes. */ + removed_all &= trim_subnode_hash(&node->sub_nodes, latest_any_var, + scratch_pool); - /* Has the section explicitly determined an access? */ - conclusive = authz_access_is_determined(b->allow, b->deny, - b->required_access); + if (node->pattern_sub_nodes) + { + if (trim_tree(node->pattern_sub_nodes->any, latest_any_var, + scratch_pool)) + node->pattern_sub_nodes->any = NULL; + else + removed_all = FALSE; - /* Is access granted OR inconclusive? */ - b->access = authz_access_is_granted(b->allow, b->deny, - b->required_access) - || !conclusive; + if (trim_tree(node->pattern_sub_nodes->any_var, latest_any_var, + scratch_pool)) + node->pattern_sub_nodes->any_var = NULL; + else + removed_all = FALSE; + + removed_all &= trim_subnode_array(&node->pattern_sub_nodes->prefixes, + latest_any_var, scratch_pool); + removed_all &= trim_subnode_array(&node->pattern_sub_nodes->suffixes, + latest_any_var, scratch_pool); + removed_all &= trim_subnode_array(&node->pattern_sub_nodes->complex, + latest_any_var, scratch_pool); + + /* Trim the tree as much as possible to speed up lookup(). */ + if (removed_all) + node->pattern_sub_nodes = NULL; + } - /* As long as access isn't conclusively denied, carry on. */ - return b->access; + return removed_all; } +/* Forward declaration ... */ +static void +finalize_tree(node_t *node, + limited_rights_t *sum, + apr_pool_t *scratch_pool); -/* Validate access to the given user for the given path. This - * function checks rules for exactly the given path, and first tries - * to access a section specific to the given repository before falling - * back to pan-repository rules. - * - * Update *access_granted to inform the caller of the outcome of the - * lookup. Return a boolean indicating whether the access rights were - * successfully determined. +/* Call finalize_tree() on all elements in the HASH of node_t *, passing + * SUM along. HASH may be NULL. Use SCRATCH_POOL for temporary allocations. */ -static svn_boolean_t -authz_get_path_access(svn_config_t *cfg, const char *repos_name, - const char *path, const char *user, - svn_repos_authz_access_t required_access, - svn_boolean_t *access_granted, - apr_pool_t *pool) +static void +finalize_subnode_hash(apr_hash_t *hash, + limited_rights_t *sum, + apr_pool_t *scratch_pool) { - const char *qualified_path; - struct authz_lookup_baton baton = { 0 }; - - baton.config = cfg; - baton.user = user; - - /* Try to locate a repository-specific block first. */ - qualified_path = apr_pstrcat(pool, repos_name, ":", path, SVN_VA_NULL); - svn_config_enumerate2(cfg, qualified_path, - authz_parse_line, &baton, pool); - - *access_granted = authz_access_is_granted(baton.allow, baton.deny, - required_access); + if (hash) + { + apr_hash_index_t *hi; + for (hi = apr_hash_first(scratch_pool, hash); + hi; + hi = apr_hash_next(hi)) + finalize_tree(apr_hash_this_val(hi), sum, scratch_pool); + } +} - /* If the first test has determined access, stop now. */ - if (authz_access_is_determined(baton.allow, baton.deny, - required_access)) - return TRUE; +/* Call finalize_up_tree() on all elements in the ARRAY of node_t *, + * passing SUM along. ARRAY may be NULL. Use SCRATCH_POOL for temporary + * allocations. + */ +static void +finalize_subnode_array(apr_array_header_t *array, + limited_rights_t *sum, + apr_pool_t *scratch_pool) +{ + if (array) + { + int i; + for (i = 0; i < array->nelts; ++i) + finalize_tree(APR_ARRAY_IDX(array, i, sorted_pattern_t).node, sum, + scratch_pool); + } +} - /* No repository specific rule, try pan-repository rules. */ - svn_config_enumerate2(cfg, path, authz_parse_line, &baton, pool); +/* Link prefixes within the sorted ARRAY. */ +static void +link_prefix_patterns(apr_array_header_t *array) +{ + int i; + if (!array) + return; - *access_granted = authz_access_is_granted(baton.allow, baton.deny, - required_access); - return authz_access_is_determined(baton.allow, baton.deny, - required_access); + for (i = 1; i < array->nelts; ++i) + { + sorted_pattern_t *prev + = &APR_ARRAY_IDX(array, i - 1, sorted_pattern_t); + sorted_pattern_t *pattern + = &APR_ARRAY_IDX(array, i, sorted_pattern_t); + + /* Does PATTERN potentially have a prefix in ARRAY? + * If so, at least the first char must match with the predecessor's + * because the array is sorted by that string. */ + if (prev->node->segment.data[0] != pattern->node->segment.data[0]) + continue; + + /* Only the predecessor or any of its prefixes can be the closest + * prefix to PATTERN. */ + for ( ; prev; prev = prev->next) + if ( prev->node->segment.len < pattern->node->segment.len + && !memcmp(prev->node->segment.data, + pattern->node->segment.data, + prev->node->segment.len)) + { + pattern->next = prev; + break; + } + } } - -/* Validate access to the given user for the subtree starting at the - * given path. This function walks the whole authz file in search of - * rules applying to paths in the requested subtree which deny the - * requested access. - * - * As soon as one is found, or else when the whole ACL file has been - * searched, return the updated authorization status. +/* Recursively finalization the tree node properties for NODE. Update SUM + * (of NODE's parent) by combining it with the recursive access rights info + * on NODE. Use SCRATCH_POOL for temporary allocations. */ -static svn_boolean_t -authz_get_tree_access(svn_config_t *cfg, const char *repos_name, - const char *path, const char *user, - svn_repos_authz_access_t required_access, - apr_pool_t *pool) +static void +finalize_tree(node_t *node, + limited_rights_t *sum, + apr_pool_t *scratch_pool) { - struct authz_lookup_baton baton = { 0 }; + limited_rights_t *local_sum = &node->rights; - baton.config = cfg; - baton.user = user; - baton.required_access = required_access; - baton.repos_path = path; - baton.qualified_repos_path = apr_pstrcat(pool, repos_name, - ":", path, SVN_VA_NULL); - /* Default to access granted if no rules say otherwise. */ - baton.access = TRUE; + /* For convenience, we allow NODE to be NULL: */ + if (!node) + return; - svn_config_enumerate_sections2(cfg, authz_parse_section, - &baton, pool); + /* Sum of rights at NODE - so far. */ + if (has_local_rule(local_sum)) + { + local_sum->max_rights = local_sum->access.rights; + local_sum->min_rights = local_sum->access.rights; + } + else + { + local_sum->min_rights = authz_access_write; + local_sum->max_rights = authz_access_none; + } - return baton.access; -} + /* Process all sub-nodes. */ + finalize_subnode_hash(node->sub_nodes, local_sum, scratch_pool); + if (node->pattern_sub_nodes) + { + finalize_tree(node->pattern_sub_nodes->any, local_sum, scratch_pool); + finalize_tree(node->pattern_sub_nodes->any_var, local_sum, scratch_pool); + + finalize_subnode_array(node->pattern_sub_nodes->prefixes, local_sum, + scratch_pool); + finalize_subnode_array(node->pattern_sub_nodes->suffixes, local_sum, + scratch_pool); + finalize_subnode_array(node->pattern_sub_nodes->complex, local_sum, + scratch_pool); + + /* Link up the prefixes / suffixes. */ + link_prefix_patterns(node->pattern_sub_nodes->prefixes); + link_prefix_patterns(node->pattern_sub_nodes->suffixes); + } -/* Callback to parse sections of the configuration file, looking for - any kind of granted access. Implements the - svn_config_section_enumerator2_t interface. */ -static svn_boolean_t -authz_get_any_access_parser_cb(const char *section_name, void *baton, - apr_pool_t *pool) -{ - struct authz_lookup_baton *b = baton; + /* Add our min / max info to the parent's info. + * Idempotent for parent == node (happens at root). */ + combine_right_limits(sum, local_sum); +} - /* Does the section apply to the query? */ - if (section_name[0] == '/' - || strncmp(section_name, b->qualified_repos_path, - strlen(b->qualified_repos_path)) == 0) +/* From the authz CONFIG, extract the parts relevant to USER and REPOSITORY. + * Return the filtered rule tree. + */ +static node_t * +create_user_authz(authz_full_t *authz, + const char *repository, + const char *user, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + int i; + node_t *root = create_node(NULL, result_pool); + construction_context_t *ctx = create_construction_context(scratch_pool); + + /* Use a separate sub-pool to keep memory usage tight. */ + apr_pool_t *subpool = svn_pool_create(scratch_pool); + + /* Find all ACLs for REPOSITORY. + * Note that repo-specific rules replace global rules, + * even if they don't apply to the current user. */ + apr_array_header_t *acls = apr_array_make(subpool, authz->acls->nelts, + sizeof(authz_acl_t *)); + for (i = 0; i < authz->acls->nelts; ++i) { - b->allow = b->deny = svn_authz_none; + const authz_acl_t *acl = &APR_ARRAY_IDX(authz->acls, i, authz_acl_t); + if (svn_authz__acl_applies_to_repo(acl, repository)) + { + /* ACLs in the AUTHZ are sorted by path and repository. + * So, if there is a rule for the repo and a global rule for the + * same path, we will detect them here. */ + if (acls->nelts) + { + const authz_acl_t *prev_acl + = APR_ARRAY_IDX(acls, acls->nelts - 1, const authz_acl_t *); + if (svn_authz__compare_paths(&prev_acl->rule, &acl->rule) == 0) + { + SVN_ERR_ASSERT_NO_RETURN(!strcmp(prev_acl->rule.repos, + AUTHZ_ANY_REPOSITORY)); + SVN_ERR_ASSERT_NO_RETURN(strcmp(acl->rule.repos, + AUTHZ_ANY_REPOSITORY)); + apr_array_pop(acls); + } + } + + APR_ARRAY_PUSH(acls, const authz_acl_t *) = acl; + } + } - svn_config_enumerate2(b->config, section_name, - authz_parse_line, baton, pool); - b->access = authz_access_is_granted(b->allow, b->deny, - b->required_access); + /* Filtering and tree construction. */ + for (i = 0; i < acls->nelts; ++i) + process_acl(ctx, APR_ARRAY_IDX(acls, i, const authz_acl_t *), + root, repository, user, result_pool, subpool); - /* Continue as long as we don't find a determined, granted access. */ - return !(b->access - && authz_access_is_determined(b->allow, b->deny, - b->required_access)); + /* If there is no relevant rule at the root node, the "no access" default + * applies. Give it a SEQUENCE_NUMBER that will never overrule others. */ + if (!has_local_rule(&root->rights)) + { + root->rights.access.sequence_number = 0; + root->rights.access.rights = authz_access_none; } - return TRUE; -} - + /* Trim the tree. + * + * We can't do pattern comparison, so for most pattern rules we cannot + * say that a set of rules "eclipses" / overrides a given other set of + * rules for all possible paths. That limits the accuracy of our check + * for recursive access in similar ways than for non-pattern rules. + * + * However, the user expects a rule ending with "**" to eclipse any older + * rule in that sub-tree recursively. So, this trim function removes + * eclipsed nodes from the tree. + */ + svn_pool_clear(subpool); + trim_tree(root, NO_SEQUENCE_NUMBER, subpool); -/* Walk through the authz CFG to check if USER has the REQUIRED_ACCESS - * to any path within the REPOSITORY. Return TRUE if so. Use POOL - * for temporary allocations. */ -static svn_boolean_t -authz_get_any_access(svn_config_t *cfg, const char *repos_name, - const char *user, - svn_repos_authz_access_t required_access, - apr_pool_t *pool) -{ - struct authz_lookup_baton baton = { 0 }; - - baton.config = cfg; - baton.user = user; - baton.required_access = required_access; - baton.access = FALSE; /* Deny access by default. */ - baton.repos_path = "/"; - baton.qualified_repos_path = apr_pstrcat(pool, repos_name, - ":/", SVN_VA_NULL); - - /* We could have used svn_config_enumerate2 for "repos_name:/". - * However, this requires access for root explicitly (which the user - * may not always have). So we end up enumerating the sections in - * the authz CFG and stop on the first match with some access for - * this user. */ - svn_config_enumerate_sections2(cfg, authz_get_any_access_parser_cb, - &baton, pool); - - /* If walking the configuration was inconclusive, deny access. */ - if (!authz_access_is_determined(baton.allow, - baton.deny, baton.required_access)) - return FALSE; + /* Calculate recursive rights. + * + * This is a bottom-up calculation of the range of access rights + * specified anywhere in the respective sub-tree, including the base + * node itself. + * + * To prevent additional finalization passes, we piggy-back the addition + * of the ordering links of the prefix and suffix sub-node rules. + */ + svn_pool_clear(subpool); + finalize_tree(root, &root->rights, subpool); - return baton.access; + /* Done. */ + svn_pool_destroy(subpool); + return root; } - -/*** Validating the authz file. ***/ +/*** Lookup. ***/ -/* Check for errors in GROUP's definition of CFG. The errors - * detected are references to non-existent groups and circular - * dependencies between groups. If an error is found, return - * SVN_ERR_AUTHZ_INVALID_CONFIG. Use POOL for temporary - * allocations only. - * - * CHECKED_GROUPS should be an empty (it is used for recursive calls). - */ -static svn_error_t * -authz_group_walk(svn_config_t *cfg, - const char *group, - apr_hash_t *checked_groups, - apr_pool_t *pool) +/* Reusable lookup state object. It is easy to pass to functions and + * recycling it between lookups saves significant setup costs. */ +typedef struct lookup_state_t { - const char *value; - apr_array_header_t *list; - int i; + /* Rights immediately applying to this node and limits to the rights to + * any sub-path. */ + limited_rights_t rights; - svn_config_get(cfg, &value, "groups", group, NULL); - /* Having a non-existent group in the ACL configuration might be the - sign of a typo. Refuse to perform authz on uncertain rules. */ - if (!value) - return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "An authz rule refers to group '%s', " - "which is undefined", - group); + /* Nodes applying to the path followed so far. */ + apr_array_header_t *current; - list = svn_cstring_split(value, ",", TRUE, pool); + /* Temporary array containing the nodes applying to the next path + * segment (used to build up the next contents of CURRENT). */ + apr_array_header_t *next; - for (i = 0; i < list->nelts; i++) - { - const char *group_user = APR_ARRAY_IDX(list, i, char *); + /* Scratch pad for path operations. */ + svn_stringbuf_t *scratch_pad; - /* If the 'user' is a subgroup, recurse into it. */ - if (*group_user == '@') - { - /* A circular dependency between groups is a Bad Thing. We - don't do authz with invalid ACL files. */ - if (svn_hash_gets(checked_groups, &group_user[1])) - return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, - NULL, - "Circular dependency between " - "groups '%s' and '%s'", - &group_user[1], group); - - /* Add group to hash of checked groups. */ - svn_hash_sets(checked_groups, &group_user[1], ""); - - /* Recurse on that group. */ - SVN_ERR(authz_group_walk(cfg, &group_user[1], - checked_groups, pool)); - - /* Remove group from hash of checked groups, so that we don't - incorrectly report an error if we see it again as part of - another group. */ - svn_hash_sets(checked_groups, &group_user[1], NULL); - } - else if (*group_user == '&') - { - const char *alias; - - svn_config_get(cfg, &alias, "aliases", &group_user[1], NULL); - /* Having a non-existent alias in the ACL configuration might be the - sign of a typo. Refuse to perform authz on uncertain rules. */ - if (!alias) - return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "An authz rule refers to alias '%s', " - "which is undefined", - &group_user[1]); - } - } + /* After each lookup iteration, CURRENT and PARENT_RIGHTS will + * apply to this path. */ + svn_stringbuf_t *parent_path; - return SVN_NO_ERROR; -} + /* Rights that apply at PARENT_PATH, if PARENT_PATH is not empty. */ + limited_rights_t parent_rights; +} lookup_state_t; -/* Callback to perform some simple sanity checks on an authz rule. - * - * - If RULE_MATCH_STRING references a group or an alias, verify that - * the group or alias definition exists. - * - If RULE_MATCH_STRING specifies a token (starts with $), verify - * that the token name is valid. - * - If RULE_MATCH_STRING is using inversion, verify that it isn't - * doing it more than once within the one rule, and that it isn't - * "~*", as that would never match. - * - Check that VALUE part of the rule specifies only allowed rule - * flag characters ('r' and 'w'). - * - * Return TRUE if the rule has no errors. Use BATON for context and - * error reporting. - */ -static svn_boolean_t authz_validate_rule(const char *rule_match_string, - const char *value, - void *baton, - apr_pool_t *pool) +/* Constructor for lookup_state_t. */ +static lookup_state_t * +create_lookup_state(apr_pool_t *result_pool) { - const char *val; - const char *match = rule_match_string; - struct authz_validate_baton *b = baton; + lookup_state_t *state = apr_pcalloc(result_pool, sizeof(*state)); + + state->next = apr_array_make(result_pool, 4, sizeof(node_t *)); + state->current = apr_array_make(result_pool, 4, sizeof(node_t *)); - /* Make sure the user isn't using double-negatives. */ - if (match[0] == '~') - { - /* Bump the pointer past the inversion for the other checks. */ - match++; + /* Virtually all path segments should fit into this buffer. If they + * don't, the buffer gets automatically reallocated. + * + * Using a smaller initial size would be fine as well but does not + * buy us much for the increased risk of being expanded anyway - at + * some extra cost. */ + state->scratch_pad = svn_stringbuf_create_ensure(200, result_pool); - /* Another inversion is a double negative; we can't not stop. */ - if (match[0] == '~') - { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Rule '%s' has more than one " - "inversion; double negatives are " - "not permitted.", - rule_match_string); - return FALSE; - } + /* Most paths should fit into this buffer. The same rationale as + * above applies. */ + state->parent_path = svn_stringbuf_create_ensure(200, result_pool); - /* Make sure that the rule isn't "~*", which won't ever match. */ - if (strcmp(match, "*") == 0) - { - b->err = svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Authz rules with match string '~*' " - "are not allowed, because they never " - "match anyone."); - return FALSE; - } - } + return state; +} - /* If the rule applies to a group, check its existence. */ - if (match[0] == '@') +/* Clear the current contents of STATE and re-initialize it for ROOT. + * Check whether we can reuse a previous parent path lookup to shorten + * the current PATH walk. Return the full or remaining portion of + * PATH, respectively. PATH must not be NULL. */ +static const char * +init_lockup_state(lookup_state_t *state, + node_t *root, + const char *path) +{ + apr_size_t len = strlen(path); + if ( (len > state->parent_path->len) + && state->parent_path->len + && (path[state->parent_path->len] == '/') + && !memcmp(path, state->parent_path->data, state->parent_path->len)) { - const char *group = &match[1]; + /* The PARENT_PATH of the previous lookup is actually a parent path + * of PATH. The CURRENT node list already matches the parent path + * and we only have to set the correct rights info. */ + state->rights = state->parent_rights; - svn_config_get(b->config, &val, "groups", group, NULL); - - /* Having a non-existent group in the ACL configuration might be - the sign of a typo. Refuse to perform authz on uncertain - rules. */ - if (!val) - { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "An authz rule refers to group " - "'%s', which is undefined", - rule_match_string); - return FALSE; - } + /* Tell the caller where to proceed. */ + return path + state->parent_path->len; } - /* If the rule applies to an alias, check its existence. */ - if (match[0] == '&') - { - const char *alias = &match[1]; + /* Start lookup at ROOT for the full PATH. */ + state->rights = root->rights; + state->parent_rights = root->rights; - svn_config_get(b->config, &val, "aliases", alias, NULL); + apr_array_clear(state->next); + apr_array_clear(state->current); + APR_ARRAY_PUSH(state->current, node_t *) = root; - if (!val) - { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "An authz rule refers to alias " - "'%s', which is undefined", - rule_match_string); - return FALSE; - } - } + /* Var-segment rules match empty segments as well */ + if (root->pattern_sub_nodes && root->pattern_sub_nodes->any_var) + { + node_t *node = root->pattern_sub_nodes->any_var; - /* If the rule specifies a token, check its validity. */ - if (match[0] == '$') - { - const char *token_name = &match[1]; + /* This is non-recursive due to ACL normalization. */ + combine_access(&state->rights, &node->rights); + combine_right_limits(&state->rights, &node->rights); + APR_ARRAY_PUSH(state->current, node_t *) = node; + } + + svn_stringbuf_setempty(state->parent_path); + svn_stringbuf_setempty(state->scratch_pad); - if ((strcmp(token_name, "anonymous") != 0) - && (strcmp(token_name, "authenticated") != 0)) + return path; +} + +/* Add NODE to the list of NEXT nodes in STATE. NODE may be NULL in which + * case this is a no-op. Also update and aggregate the access rights data + * for the next path segment. + */ +static void +add_next_node(lookup_state_t *state, + node_t *node) +{ + /* Allowing NULL nodes simplifies the caller. */ + if (node) + { + /* The rule with the highest sequence number is the one that applies. + * Not all nodes that we are following have rules that apply directly + * to this path but are mere intermediates that may only have some + * matching deep sub-node. */ + combine_access(&state->rights, &node->rights); + + /* The rule tree node can be seen as an overlay of all the nodes that + * we are following. Any of them _may_ match eventually, so the min/ + * max possible access rights are a combination of all these sub-trees. + */ + combine_right_limits(&state->rights, &node->rights); + + /* NODE is now enlisted as a (potential) match for the next segment. */ + APR_ARRAY_PUSH(state->next, node_t *) = node; + + /* Variable length sub-segment sequences apply to the same node as + * they match empty sequences as well. */ + if (node->pattern_sub_nodes && node->pattern_sub_nodes->any_var) { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Unrecognized authz token '%s'.", - rule_match_string); - return FALSE; + node = node->pattern_sub_nodes->any_var; + + /* This is non-recursive due to ACL normalization. */ + combine_access(&state->rights, &node->rights); + combine_right_limits(&state->rights, &node->rights); + APR_ARRAY_PUSH(state->next, node_t *) = node; } } +} - val = value; +/* If PREFIX is indeed a prefix (or exact match) or SEGMENT, add the + * node in PREFIX to STATE. */ +static void +add_if_prefix_matches(lookup_state_t *state, + const sorted_pattern_t *prefix, + const svn_stringbuf_t *segment) +{ + node_t *node = prefix->node; + if ( node->segment.len <= segment->len + && !memcmp(node->segment.data, segment->data, node->segment.len)) + add_next_node(state, node); +} - while (*val) +/* Scan the PREFIXES array of node_t* for all entries whose SEGMENT members + * are prefixes of SEGMENT. Add these to STATE for the next tree level. */ +static void +add_prefix_matches(lookup_state_t *state, + const svn_stringbuf_t *segment, + apr_array_header_t *prefixes) +{ + /* Index of the first node that might be a match. All matches will + * be at this and the immediately following indexes. */ + int i = svn_sort__bsearch_lower_bound(prefixes, segment->data, + compare_node_path_segment); + + /* The entry we found may be an exact match (but not a true prefix). + * The prefix matching test will still work. */ + if (i < prefixes->nelts) + add_if_prefix_matches(state, + &APR_ARRAY_IDX(prefixes, i, sorted_pattern_t), + segment); + + /* The immediate predecessor may be a true prefix and all potential + * prefixes can be found following the NEXT links between the array + * indexes. */ + if (i > 0) { - if (*val != 'r' && *val != 'w' && ! svn_ctype_isspace(*val)) + sorted_pattern_t *pattern; + for (pattern = &APR_ARRAY_IDX(prefixes, i - 1, sorted_pattern_t); + pattern; + pattern = pattern->next) { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "The character '%c' in rule '%s' is not " - "allowed in authz rules", *val, - rule_match_string); - return FALSE; + add_if_prefix_matches(state, pattern, segment); } - - ++val; } - - return TRUE; } -/* Callback to check ALIAS's definition for validity. Use - BATON for context and error reporting. */ -static svn_boolean_t authz_validate_alias(const char *alias, - const char *value, - void *baton, - apr_pool_t *pool) +/* Scan the PATTERNS array of node_t* for all entries whose SEGMENT members + * (usually containing wildcards) match SEGMENT. Add these to STATE for the + * next tree level. */ +static void +add_complex_matches(lookup_state_t *state, + const svn_stringbuf_t *segment, + apr_array_header_t *patterns) { - /* No checking at the moment, every alias is valid */ - return TRUE; + int i; + for (i = 0; i < patterns->nelts; ++i) + { + node_t *node = APR_ARRAY_IDX(patterns, i, sorted_pattern_t).node; + if (0 == apr_fnmatch(node->segment.data, segment->data, 0)) + add_next_node(state, node); + } } - -/* Callback to check GROUP's definition for cyclic dependancies. Use - BATON for context and error reporting. */ -static svn_boolean_t authz_validate_group(const char *group, - const char *value, - void *baton, - apr_pool_t *pool) +/* Extract the next segment from PATH and copy it into SEGMENT, whose current + * contents get overwritten. Empty paths ("") are supported and leading '/' + * segment separators will be interpreted as an empty segment (""). Non- + * normalizes parts, i.e. sequences of '/', will be treated as a single '/'. + * + * Return the start of the next segment within PATH, skipping the '/' + * separator(s). Return NULL, if there are no further segments. + * + * The caller (only called by lookup(), ATM) must ensure that SEGMENT has + * enough room to store all of PATH. + */ +static const char * +next_segment(svn_stringbuf_t *segment, + const char *path) { - struct authz_validate_baton *b = baton; + apr_size_t len; + char c; + + /* Read and scan PATH for NUL and '/' -- whichever comes first. */ + for (len = 0, c = *path; c; c = path[++len]) + if (c == '/') + { + /* End of segment. */ + segment->data[len] = 0; + segment->len = len; + + /* If PATH is not normalized, this is where we skip whole sequences + * of separators. */ + while (path[++len] == '/') + ; + + /* Continue behind the last separator in the sequence. We will + * treat trailing '/' as indicating an empty trailing segment. + * Therefore, we never have to return NULL here. */ + return path + len; + } + else + { + /* Copy segment contents directly into the result buffer. + * On many architectures, this is almost or entirely for free. */ + segment->data[len] = c; + } + + /* No separator found, so all of PATH has been the last segment. */ + segment->data[len] = 0; + segment->len = len; + + /* Tell the caller that this has been the last segment. */ + return NULL; +} - b->err = authz_group_walk(b->config, group, apr_hash_make(pool), pool); - if (b->err) - return FALSE; +/* Starting at the respective user's authz root node provided with STATE, + * follow PATH and return TRUE, iff the REQUIRED access has been granted to + * that user for this PATH. REQUIRED must not contain svn_authz_recursive. + * If RECURSIVE is set, all paths in the sub-tree at and below PATH must + * have REQUIRED access. PATH does not need to be normalized, may be empty + * but must not be NULL. + */ +static svn_boolean_t +lookup(lookup_state_t *state, + const char *path, + authz_access_t required, + svn_boolean_t recursive, + apr_pool_t *scratch_pool) +{ + /* Create a scratch pad large enough to hold any of PATH's segments. */ + apr_size_t path_len = strlen(path); + svn_stringbuf_ensure(state->scratch_pad, path_len); - return TRUE; -} + /* Normalize start and end of PATH. Most paths will be fully normalized, + * so keep the overhead as low as possible. */ + if (path_len && path[path_len-1] == '/') + { + do + { + --path_len; + } + while (path_len && path[path_len-1] == '/'); + path = apr_pstrmemdup(scratch_pool, path, path_len); + } + while (path[0] == '/') + ++path; /* Don't update PATH_LEN as we won't need it anymore. */ -/* Callback to check the contents of the configuration section given - by NAME. Use BATON for context and error reporting. */ -static svn_boolean_t authz_validate_section(const char *name, - void *baton, - apr_pool_t *pool) -{ - struct authz_validate_baton *b = baton; - - /* Use the group checking callback for the "groups" section... */ - if (strcmp(name, "groups") == 0) - svn_config_enumerate2(b->config, name, authz_validate_group, - baton, pool); - /* ...and the alias checking callback for "aliases"... */ - else if (strcmp(name, "aliases") == 0) - svn_config_enumerate2(b->config, name, authz_validate_alias, - baton, pool); - /* ...but for everything else use the rule checking callback. */ - else + /* Actually walk the path rule tree following PATH until we run out of + * either tree or PATH. */ + while (state->current->nelts && path) { - /* Validate the section's name. Skip the optional REPOS_NAME. */ - const char *fspath = strchr(name, ':'); - if (fspath) - fspath++; - else - fspath = name; - if (! svn_fspath__is_canonical(fspath)) + apr_array_header_t *temp; + int i; + svn_stringbuf_t *segment = state->scratch_pad; + + /* Shortcut 1: We could nowhere find enough rights in this sub-tree. */ + if ((state->rights.max_rights & required) != required) + return FALSE; + + /* Shortcut 2: We will find enough rights everywhere in this sub-tree. */ + if ((state->rights.min_rights & required) == required) + return TRUE; + + /* Extract the next segment. */ + path = next_segment(segment, path); + + /* Initial state for this segment. */ + apr_array_clear(state->next); + state->rights.access.sequence_number = NO_SEQUENCE_NUMBER; + state->rights.access.rights = authz_access_none; + + /* These init values ensure that the first node's value will be used + * when combined with them. If there is no first node, + * state->access.sequence_number remains unchanged and we will use + * the parent's (i.e. inherited) access rights. */ + state->rights.min_rights = authz_access_write; + state->rights.max_rights = authz_access_none; + + /* Update the PARENT_PATH member in STATE to match the nodes in + * CURRENT at the end of this iteration, i.e. if and when NEXT + * has become CURRENT. */ + if (path) { - b->err = svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Section name '%s' contains non-canonical " - "fspath '%s'", - name, fspath); - return FALSE; + svn_stringbuf_appendbyte(state->parent_path, '/'); + svn_stringbuf_appendbytes(state->parent_path, segment->data, + segment->len); } - svn_config_enumerate2(b->config, name, authz_validate_rule, - baton, pool); + /* Scan follow all alternative routes to the next level. */ + for (i = 0; i < state->current->nelts; ++i) + { + node_t *node = APR_ARRAY_IDX(state->current, i, node_t *); + if (node->sub_nodes) + add_next_node(state, apr_hash_get(node->sub_nodes, segment->data, + segment->len)); + + /* Process alternative, wildcard-based sub-nodes. */ + if (node->pattern_sub_nodes) + { + add_next_node(state, node->pattern_sub_nodes->any); + + /* If the current node represents a "**" pattern, it matches + * to all levels. So, add it to the list for the NEXT level. */ + if (node->pattern_sub_nodes->repeat) + add_next_node(state, node); + + /* Find all prefix pattern matches. */ + if (node->pattern_sub_nodes->prefixes) + add_prefix_matches(state, segment, + node->pattern_sub_nodes->prefixes); + + if (node->pattern_sub_nodes->complex) + add_complex_matches(state, segment, + node->pattern_sub_nodes->complex); + + /* Find all suffux pattern matches. + * This must be the last check as it destroys SEGMENT. */ + if (node->pattern_sub_nodes->suffixes) + { + /* Suffixes behave like reversed prefixes. */ + svn_authz__reverse_string(segment->data, segment->len); + add_prefix_matches(state, segment, + node->pattern_sub_nodes->suffixes); + } + } + } + + /* If no rule applied to this SEGMENT directly, the parent rights + * will apply to at least the SEGMENT node itself and possibly + * other parts deeper in it's subtree. */ + if (!has_local_rule(&state->rights)) + { + state->rights.access = state->parent_rights.access; + state->rights.min_rights &= state->parent_rights.access.rights; + state->rights.max_rights |= state->parent_rights.access.rights; + } + + /* The list of nodes for SEGMENT is now complete. If we need to + * continue, make it the current and put the old one into the recycler. + * + * If this is the end of the path, keep the parent path and rights in + * STATE as are such that sibling lookups will benefit from it. + */ + if (path) + { + temp = state->current; + state->current = state->next; + state->next = temp; + + /* In STATE, PARENT_PATH, PARENT_RIGHTS and CURRENT are now in sync. */ + state->parent_rights = state->rights; + } } - if (b->err) - return FALSE; + /* If we check recursively, none of the (potential) sub-paths must have + * less than the REQUIRED access rights. "Potential" because we don't + * verify that the respective paths actually exist in the repository. + */ + if (recursive) + return (state->rights.min_rights & required) == required; - return TRUE; + /* Return whether the access rights on PATH fully include REQUIRED. */ + return (state->rights.access.rights & required) == required; } + + +/*** The authz data structure. ***/ -svn_error_t * -svn_repos__authz_validate(svn_authz_t *authz, apr_pool_t *pool) +/* An entry in svn_authz_t's USER_RULES cache. All members must be + * allocated in the POOL and the latter has to be cleared / destroyed + * before overwriting the entries' contents. + */ +struct authz_user_rules_t { - struct authz_validate_baton baton = { 0 }; + /* User name for which we filtered the rules. + * User NULL for the anonymous user. */ + const char *user; - baton.err = SVN_NO_ERROR; - baton.config = authz->cfg; + /* Repository name for which we filtered the rules. + * May be empty but never NULL for used entries. */ + const char *repository; - /* Step through the entire rule file stopping on error. */ - svn_config_enumerate_sections2(authz->cfg, authz_validate_section, - &baton, pool); - SVN_ERR(baton.err); + /* The combined min/max rights USER has on REPOSITORY. */ + authz_rights_t global_rights; - return SVN_NO_ERROR; -} + /* Root of the filtered path rule tree. + * Will remain NULL until the first usage. */ + node_t *root; + /* Reusable lookup state instance. */ + lookup_state_t *lookup_state; -/* Retrieve the file at DIRENT (contained in a repo) then parse it as a config - * file placing the result into CFG_P allocated in POOL. - * - * If DIRENT cannot be parsed as a config file then an error is returned. The - * contents of CFG_P is then undefined. If MUST_EXIST is TRUE, a missing - * authz file is also an error. The CASE_SENSITIVE controls the lookup - * behavior for section and option names alike. - * - * SCRATCH_POOL will be used for temporary allocations. */ -static svn_error_t * -authz_retrieve_config_repo(svn_config_t **cfg_p, - const char *dirent, - svn_boolean_t must_exist, - svn_boolean_t case_sensitive, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) + /* Pool from which all data within this struct got allocated. + * Can be destroyed or cleaned up with no further side-effects. */ + apr_pool_t *pool; +}; + +/* Return TRUE, iff AUTHZ matches the pair of REPOS_NAME and USER. + * Note that USER may be NULL. + */ +static svn_boolean_t +matches_filtered_tree(const authz_user_rules_t *authz, + const char *repos_name, + const char *user) { - svn_error_t *err; - svn_repos_t *repos; - const char *repos_root_dirent; - const char *fs_path; - svn_fs_t *fs; - svn_fs_root_t *root; - svn_revnum_t youngest_rev; - svn_node_kind_t node_kind; - svn_stream_t *contents; - - /* Search for a repository in the full path. */ - repos_root_dirent = svn_repos_find_root_path(dirent, scratch_pool); - if (!repos_root_dirent) - return svn_error_createf(SVN_ERR_RA_LOCAL_REPOS_NOT_FOUND, NULL, - "Unable to find repository at '%s'", dirent); - - /* Attempt to open a repository at repos_root_dirent. */ - SVN_ERR(svn_repos_open3(&repos, repos_root_dirent, NULL, scratch_pool, - scratch_pool)); - - fs_path = &dirent[strlen(repos_root_dirent)]; - - /* Root path is always a directory so no reason to go any further */ - if (*fs_path == '\0') - return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, - "'/' is not a file in repo '%s'", - repos_root_dirent); - - /* We skip some things that are non-important for how we're going to use - * this repo connection. We do not set any capabilities since none of - * the current ones are important for what we're doing. We also do not - * setup the environment that repos hooks would run under since we won't - * be triggering any. */ - - /* Get the filesystem. */ - fs = svn_repos_fs(repos); - - /* Find HEAD and the revision root */ - SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, scratch_pool)); - SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, scratch_pool)); - - SVN_ERR(svn_fs_check_path(&node_kind, root, fs_path, scratch_pool)); - if (node_kind == svn_node_none) + /* Does the user match? */ + if (user) { - if (!must_exist) - { - SVN_ERR(svn_config_create2(cfg_p, case_sensitive, case_sensitive, - result_pool)); - return SVN_NO_ERROR; - } - else - { - return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, - "'%s' path not found in repo '%s'", fs_path, - repos_root_dirent); - } + if (authz->user == NULL || strcmp(user, authz->user)) + return FALSE; } - else if (node_kind != svn_node_file) - { - return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, - "'%s' is not a file in repo '%s'", fs_path, - repos_root_dirent); - } - - SVN_ERR(svn_fs_file_contents(&contents, root, fs_path, scratch_pool)); - err = svn_config_parse(cfg_p, contents, case_sensitive, case_sensitive, - result_pool); - - /* Add the URL to the error stack since the parser doesn't have it. */ - if (err != SVN_NO_ERROR) - return svn_error_createf(err->apr_err, err, - "Error while parsing config file: '%s' in repo '%s':", - fs_path, repos_root_dirent); + else if (authz->user != NULL) + return FALSE; - return SVN_NO_ERROR; + /* Does the repository match as well? */ + return strcmp(repos_name, authz->repository) == 0; } -svn_error_t * -svn_repos__retrieve_config(svn_config_t **cfg_p, - const char *path, - svn_boolean_t must_exist, - svn_boolean_t case_sensitive, - apr_pool_t *pool) +/* Check if AUTHZ's already contains a path rule tree filtered for this + * USER, REPOS_NAME combination. If that does not exist, yet, create one + * but don't construct the actual filtered tree, yet. + */ +static authz_user_rules_t * +get_user_rules(svn_authz_t *authz, + const char *repos_name, + const char *user) { - if (svn_path_is_url(path)) + apr_pool_t *pool; + + /* Search our cache for a suitable previously filtered tree. */ + if (authz->filtered) { - const char *dirent; - svn_error_t *err; - apr_pool_t *scratch_pool = svn_pool_create(pool); + /* Is this a suitable filtered tree? */ + if (matches_filtered_tree(authz->filtered, repos_name, user)) + return authz->filtered; - err = svn_uri_get_dirent_from_file_url(&dirent, path, scratch_pool); + /* Drop the old filtered tree before creating a new one. */ + svn_pool_destroy(authz->filtered->pool); + authz->filtered = NULL; + } - if (err == SVN_NO_ERROR) - err = authz_retrieve_config_repo(cfg_p, dirent, must_exist, - case_sensitive, pool, scratch_pool); + /* Global cache lookup. Filter the full model only if necessary. */ + pool = svn_pool_create(authz->pool); - /* Close the repos and streams we opened. */ - svn_pool_destroy(scratch_pool); + /* Write a new entry. */ + authz->filtered = apr_palloc(pool, sizeof(*authz->filtered)); + authz->filtered->pool = pool; + authz->filtered->repository = apr_pstrdup(pool, repos_name); + authz->filtered->user = user ? apr_pstrdup(pool, user) : NULL; + authz->filtered->lookup_state = create_lookup_state(pool); + authz->filtered->root = NULL; - return err; - } - else - { - /* Outside of repo file or Windows registry*/ - SVN_ERR(svn_config_read3(cfg_p, path, must_exist, case_sensitive, - case_sensitive, pool)); - } + svn_authz__get_global_rights(&authz->filtered->global_rights, + authz->full, user, repos_name); - return SVN_NO_ERROR; + return authz->filtered; } - -/* Callback to copy (name, value) group into the "groups" section - of another configuration. */ -static svn_boolean_t -authz_copy_group(const char *name, const char *value, - void *baton, apr_pool_t *pool) +/* In AUTHZ's user rules, construct the actual filtered tree. + * Use SCRATCH_POOL for temporary allocations. + */ +static svn_error_t * +filter_tree(svn_authz_t *authz, + apr_pool_t *scratch_pool) { - svn_config_t *authz_cfg = baton; + apr_pool_t *pool = authz->filtered->pool; + const char *repos_name = authz->filtered->repository; + const char *user = authz->filtered->user; + node_t *root; - svn_config_set(authz_cfg, SVN_CONFIG_SECTION_GROUPS, name, value); + if (filtered_pool) + { + svn_membuf_t *key = construct_filtered_key(repos_name, user, + authz->authz_id, + scratch_pool); - return TRUE; -} + /* Cache lookup. */ + SVN_ERR(svn_object_pool__lookup((void **)&root, filtered_pool, key, + pool)); -/* Copy group definitions from GROUPS_CFG to the resulting AUTHZ. - * If AUTHZ already contains any group definition, report an error. - * Use POOL for temporary allocations. */ -static svn_error_t * -authz_copy_groups(svn_authz_t *authz, svn_config_t *groups_cfg, - apr_pool_t *pool) -{ - /* Easy out: we prohibit local groups in the authz file when global - groups are being used. */ - if (svn_config_has_section(authz->cfg, SVN_CONFIG_SECTION_GROUPS)) + if (!root) + { + apr_pool_t *item_pool = svn_object_pool__new_item_pool(authz_pool); + authz_full_t *add_ref = NULL; + + /* Make sure the underlying full authz object lives as long as the + * filtered one that we are about to create. We do this by adding + * a reference to it in ITEM_POOL (which may live longer than AUTHZ). + * + * Note that we already have a reference to that full authz in + * AUTHZ->FULL. Assert that we actually don't created multiple + * instances of the same full model. + */ + svn_error_clear(svn_object_pool__lookup((void **)&add_ref, + authz_pool, authz->authz_id, + item_pool)); + SVN_ERR_ASSERT(add_ref == authz->full); + + /* Now construct the new filtered tree and cache it. */ + root = create_user_authz(authz->full, repos_name, user, item_pool, + scratch_pool); + svn_error_clear(svn_object_pool__insert((void **)&root, + filtered_pool, key, root, + item_pool, pool)); + } + } + else { - return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Authz file cannot contain any groups " - "when global groups are being used."); + root = create_user_authz(authz->full, repos_name, user, pool, + scratch_pool); } - svn_config_enumerate2(groups_cfg, SVN_CONFIG_SECTION_GROUPS, - authz_copy_group, authz->cfg, pool); + /* Write a new entry. */ + authz->filtered->root = root; return SVN_NO_ERROR; } -svn_error_t * -svn_repos__authz_read(svn_authz_t **authz_p, const char *path, - const char *groups_path, svn_boolean_t must_exist, - svn_boolean_t accept_urls, apr_pool_t *pool) + + +/* Read authz configuration data from PATH into *AUTHZ_P, allocated in + RESULT_POOL. Return the cache key in *AUTHZ_ID. If GROUPS_PATH is set, + use the global groups parsed from it. Use SCRATCH_POOL for temporary + allocations. + + PATH and GROUPS_PATH may be a dirent or an absolute file url. REPOS_HINT + may be specified to speed up access to in-repo authz files. + + If PATH or GROUPS_PATH is not a valid authz rule file, then return + SVN_AUTHZ_INVALID_CONFIG. The contents of *AUTHZ_P is then + undefined. If MUST_EXIST is TRUE, a missing authz or global groups file + is also an error. */ +static svn_error_t * +authz_read(authz_full_t **authz_p, + svn_membuf_t **authz_id, + const char *path, + const char *groups_path, + svn_boolean_t must_exist, + svn_repos_t *repos_hint, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { - svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); + svn_error_t* err = NULL; + svn_stream_t *rules_stream = NULL; + svn_stream_t *groups_stream = NULL; + svn_checksum_t *rules_checksum = NULL; + svn_checksum_t *groups_checksum = NULL; - /* Load the authz file */ - if (accept_urls) - SVN_ERR(svn_repos__retrieve_config(&authz->cfg, path, must_exist, TRUE, - pool)); - else - SVN_ERR(svn_config_read3(&authz->cfg, path, must_exist, TRUE, TRUE, - pool)); + config_access_t *config_access = + svn_repos__create_config_access(repos_hint, scratch_pool); + /* Open the main authz file */ + SVN_ERR(svn_repos__get_config(&rules_stream, &rules_checksum, config_access, + path, must_exist, scratch_pool)); + + /* Open the optional groups file */ if (groups_path) + SVN_ERR(svn_repos__get_config(&groups_stream, &groups_checksum, + config_access, groups_path, must_exist, + scratch_pool)); + + /* The authz cache is optional. */ + *authz_id = construct_authz_key(rules_checksum, groups_checksum, + result_pool); + if (authz_pool) { - svn_config_t *groups_cfg; - svn_error_t *err; + /* Cache lookup. */ + SVN_ERR(svn_object_pool__lookup((void **)authz_p, authz_pool, + *authz_id, result_pool)); - /* Load the groups file */ - if (accept_urls) - SVN_ERR(svn_repos__retrieve_config(&groups_cfg, groups_path, - must_exist, TRUE, pool)); - else - SVN_ERR(svn_config_read3(&groups_cfg, groups_path, must_exist, - TRUE, TRUE, pool)); - - /* Copy the groups from groups_cfg into authz. */ - err = authz_copy_groups(authz, groups_cfg, pool); - - /* Add the paths to the error stack since the authz_copy_groups - routine knows nothing about them. */ - if (err != SVN_NO_ERROR) - return svn_error_createf(err->apr_err, err, - "Error reading authz file '%s' with " - "groups file '%s':", path, groups_path); + /* If not found, parse and add to cache. */ + if (!*authz_p) + { + apr_pool_t *item_pool = svn_object_pool__new_item_pool(authz_pool); + + /* Parse the configuration(s) and construct the full authz model + * from it. */ + err = svn_authz__parse(authz_p, rules_stream, groups_stream, + item_pool, scratch_pool); + if (err != SVN_NO_ERROR) + { + /* That pool would otherwise never get destroyed. */ + svn_pool_destroy(item_pool); + + /* Add the URL / file name to the error stack since the parser + * doesn't have it. */ + err = svn_error_quick_wrapf(err, + "Error while parsing config file: '%s':", + path); + } + else + { + SVN_ERR(svn_object_pool__insert((void **)authz_p, authz_pool, + *authz_id, *authz_p, + item_pool, result_pool)); + } + } + } + else + { + /* Parse the configuration(s) and construct the full authz model from + * it. */ + err = svn_error_quick_wrapf(svn_authz__parse(authz_p, rules_stream, + groups_stream, + result_pool, scratch_pool), + "Error while parsing authz file: '%s':", + path); } - /* Make sure there are no errors in the configuration. */ - SVN_ERR(svn_repos__authz_validate(authz, pool)); + svn_repos__destroy_config_access(config_access); - *authz_p = authz; - return SVN_NO_ERROR; + return err; } @@ -981,12 +1628,22 @@ svn_repos__authz_read(svn_authz_t **authz_p, const char *path, /*** Public functions. ***/ svn_error_t * -svn_repos_authz_read2(svn_authz_t **authz_p, const char *path, - const char *groups_path, svn_boolean_t must_exist, - apr_pool_t *pool) +svn_repos_authz_read3(svn_authz_t **authz_p, + const char *path, + const char *groups_path, + svn_boolean_t must_exist, + svn_repos_t *repos_hint, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { - return svn_repos__authz_read(authz_p, path, groups_path, must_exist, - TRUE, pool); + svn_authz_t *authz = apr_pcalloc(result_pool, sizeof(*authz)); + authz->pool = result_pool; + + SVN_ERR(authz_read(&authz->full, &authz->authz_id, path, groups_path, + must_exist, repos_hint, result_pool, scratch_pool)); + + *authz_p = authz; + return SVN_NO_ERROR; } @@ -994,29 +1651,20 @@ svn_error_t * svn_repos_authz_parse(svn_authz_t **authz_p, svn_stream_t *stream, svn_stream_t *groups_stream, apr_pool_t *pool) { - svn_authz_t *authz = apr_palloc(pool, sizeof(*authz)); - - /* Parse the authz stream */ - SVN_ERR(svn_config_parse(&authz->cfg, stream, TRUE, TRUE, pool)); - - if (groups_stream) - { - svn_config_t *groups_cfg; - - /* Parse the groups stream */ - SVN_ERR(svn_config_parse(&groups_cfg, groups_stream, TRUE, TRUE, pool)); + apr_pool_t *scratch_pool = svn_pool_create(pool); + svn_authz_t *authz = apr_pcalloc(pool, sizeof(*authz)); + authz->pool = pool; - SVN_ERR(authz_copy_groups(authz, groups_cfg, pool)); - } + /* Parse the configuration and construct the full authz model from it. */ + SVN_ERR(svn_authz__parse(&authz->full, stream, groups_stream, pool, + scratch_pool)); - /* Make sure there are no errors in the configuration. */ - SVN_ERR(svn_repos__authz_validate(authz, pool)); + svn_pool_destroy(scratch_pool); *authz_p = authz; return SVN_NO_ERROR; } - svn_error_t * svn_repos_authz_check_access(svn_authz_t *authz, const char *repos_name, const char *path, const char *user, @@ -1024,51 +1672,58 @@ svn_repos_authz_check_access(svn_authz_t *authz, const char *repos_name, svn_boolean_t *access_granted, apr_pool_t *pool) { - const char *current_path; + const authz_access_t required = + ((required_access & svn_authz_read ? authz_access_read_flag : 0) + | (required_access & svn_authz_write ? authz_access_write_flag : 0)); + + /* Pick or create the suitable pre-filtered path rule tree. */ + authz_user_rules_t *rules = get_user_rules( + authz, + (repos_name ? repos_name : AUTHZ_ANY_REPOSITORY), + user); + + /* In many scenarios, users have uniform access to a repository + * (blanket access or no access at all). + * + * In these cases, don't bother creating or consulting the filtered tree. + */ + if ((rules->global_rights.min_access & required) == required) + { + *access_granted = TRUE; + return SVN_NO_ERROR; + } - if (!repos_name) - repos_name = ""; + if ((rules->global_rights.max_access & required) != required) + { + *access_granted = FALSE; + return SVN_NO_ERROR; + } - /* If PATH is NULL, check if the user has *any* access. */ + /* No specific path given, i.e. looking for anywhere in the tree? */ if (!path) { - *access_granted = authz_get_any_access(authz->cfg, repos_name, - user, required_access, pool); + *access_granted = + ((rules->global_rights.max_access & required) == required); return SVN_NO_ERROR; } - /* Sanity check. */ - SVN_ERR_ASSERT(path[0] == '/'); + /* Rules tree lookup */ - /* Determine the granted access for the requested path. */ - path = svn_fspath__canonicalize(path, pool); - current_path = path; + /* Did we already filter the data model? */ + if (!rules->root) + SVN_ERR(filter_tree(authz, pool)); - while (!authz_get_path_access(authz->cfg, repos_name, - current_path, user, - required_access, - access_granted, - pool)) - { - /* Stop if the loop hits the repository root with no - results. */ - if (current_path[0] == '/' && current_path[1] == '\0') - { - /* Deny access by default. */ - *access_granted = FALSE; - return SVN_NO_ERROR; - } + /* Re-use previous lookup results, if possible. */ + path = init_lockup_state(authz->filtered->lookup_state, + authz->filtered->root, path); - /* Work back to the parent path. */ - current_path = svn_fspath__dirname(current_path, pool); - } + /* Sanity check. */ + SVN_ERR_ASSERT(path[0] == '/'); - /* If the caller requested recursive access, we need to walk through - the entire authz config to see whether any child paths are denied - to the requested user. */ - if (*access_granted && (required_access & svn_authz_recursive)) - *access_granted = authz_get_tree_access(authz->cfg, repos_name, path, - user, required_access, pool); + /* Determine the granted access for the requested path. + * PATH does not need to be normalized for lockup(). */ + *access_granted = lookup(rules->lookup_state, path, required, + !!(required_access & svn_authz_recursive), pool); return SVN_NO_ERROR; } diff --git a/subversion/libsvn_repos/authz.h b/subversion/libsvn_repos/authz.h new file mode 100644 index 000000000000..7187335a94fd --- /dev/null +++ b/subversion/libsvn_repos/authz.h @@ -0,0 +1,364 @@ +/* authz.h : authz parsing and searching, private to libsvn_repos + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +#ifndef SVN_REPOS_AUTHZ_H +#define SVN_REPOS_AUTHZ_H + +#include <apr_hash.h> +#include <apr_pools.h> +#include <apr_tables.h> + +#include "svn_config.h" +#include "svn_error.h" +#include "svn_io.h" +#include "svn_repos.h" + +#include "private/svn_string_private.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +/* + * Authz and global group file parsing + */ + +/* A dictionary of rules that are specific to a particular + (user, repository) combination. */ +typedef struct authz_user_rules_t authz_user_rules_t; + + +/* Access rights in an ACL. + * + * This enum is different from and incompatible with + * svn_repos_authz_access_t, because it has different semantics and + * encodes rights that are not and should never be exposed in the + * public API. + */ +typedef enum authz_access_t +{ + /* + * Individual access flags + */ + + /* TODO: Future extension for lookup/traverse access. + authz_access_lookup_flag = 0x10, */ + + /* Read access allows listing directory entries, reading file + contents and reading properties of files and directories. */ + authz_access_read_flag = 0x20, + + /* Write access allows adding, removing and renaming directory + entries, modifying file contents and adding, removing and + modifying properties of files and directories. */ + authz_access_write_flag = 0x40, + + /* + * Combined access flags + */ + + /* No access. */ + authz_access_none = 0, + + + /* TODO: Lookup access is a synonym for the lookup flag. + authz_access_lookup = authz_access_lookup_flag, */ + + /* Read access (TODO: implies lookup access). */ + authz_access_read = authz_access_read_flag /* TODO: | authz_access_lookup */, + + /* Write access implies read (TODO: and lookup) access. */ + authz_access_write = authz_access_write_flag | authz_access_read +} authz_access_t; + + +/* Accumulated rights for (user, repository). */ +typedef struct authz_rights_t +{ + /* The lowest level of access that the user has to every + path in the repository. */ + authz_access_t min_access; + + /* The highest level of access that the user has to + any path in the repository. */ + authz_access_t max_access; +} authz_rights_t; + + +/* Accumulated global rights for a specific user. */ +typedef struct authz_global_rights_t +{ + /* The user name. */ + const char *user; + + /* Accumulated rights for this user from rules that are not + repository-specific. We use this to avoid a hash lookup for the + "any" repository rights. */ + authz_rights_t any_repos_rights; + + /* Accumulated rights for this user across all repositories. */ + authz_rights_t all_repos_rights; + + /* Accumulated rights for specific repositories. + The key is repository name, the value is an authz_rights_t*. */ + apr_hash_t *per_repos_rights; +} authz_global_rights_t; + + +/* Immutable authorization info */ +typedef struct authz_full_t +{ + /* All ACLs from the authz file, in the order of definition. */ + apr_array_header_t *acls; + + /* Globally accumulated rights for anonymous access. */ + svn_boolean_t has_anon_rights; + authz_global_rights_t anon_rights; + + /* Globally accumulated rights for authenticated users. */ + svn_boolean_t has_authn_rights; + authz_global_rights_t authn_rights; + + /* Globally accumulated rights, for all concrete users mentioned + in the authz file. The key is the user name, the value is + an authz_global_rights_t*. */ + apr_hash_t *user_rights; + + /* The pool from which all the parsed authz data is allocated. + This is the RESULT_POOL passed to svn_authz__tng_parse. + + It's a good idea to dedicate a pool for the authz structure, so + that the whole authz representation can be deallocated by + destroying the pool. */ + apr_pool_t *pool; +} authz_full_t; + + +/* Dynamic authorization info */ +struct svn_authz_t +{ + /* The parsed and pre-processed contents of the authz file. */ + authz_full_t *full; + + /* Identifies the authz model content + * (a hash value that can be used for e.g. cache lookups). */ + svn_membuf_t *authz_id; + + /* Rules filtered for a particular user-repository combination. + * May be NULL. */ + authz_user_rules_t *filtered; + + /* The pool from which all the parsed authz data is allocated. + This is the RESULT_POOL passed to svn_authz__tng_parse. + + It's a good idea to dedicate a pool for the authz structure, so + that the whole authz representation can be deallocated by + destroying the pool. */ + apr_pool_t *pool; +}; + + +/* Rule path segment descriptor. */ +typedef struct authz_rule_segment_t +{ + /* The segment type. */ + enum { + /* A literal string match. + The path segment must exactly match the pattern. + + Note: Make sure this is always the first constant in the + enumeration, otherwise rules that match the repository + root will not sort first in the ACL list and the implicit + default no-access ACE will not be applied correctly. */ + authz_rule_literal, + + /* A prefix match: a literal string followed by '*'. + The path segment must begin with the literal prefix. */ + authz_rule_prefix, + + /* A suffix match: '*' followed by a literal string. + The path segment must end with the literal suffix. + The pattern is stored reversed, so that the matching code can + perform a prefix match on the reversed path segment. */ + authz_rule_suffix, + + /* '*' + Matches any single non-empty path segment. + The pattern will be an empty string. */ + authz_rule_any_segment, + + /* '**' + Matches any sequence of zero or more path segments. + The pattern will be an empty string. */ + authz_rule_any_recursive, + + /* Any other glob/fnmatch pattern. */ + authz_rule_fnmatch + } kind; + + /* The pattern for this path segment. + Any no-op fnmatch escape sequences (i.e., those that do not + escape a wildcard or character class) are stripped from the + string. + + The pattern string will be interned; therefore, two identical + rule patterns will always contain the same pointer value and + equality can therefore be tested by comparing the pointer + values and segment kinds. */ + svn_string_t pattern; +} authz_rule_segment_t; + +/* Rule path descriptor. */ +typedef struct authz_rule_t +{ + /* The repository that this rule applies to. This will be the empty + string string if a the rule did not name a repository. The + repository name is interned. */ + const char *repos; + + /* The number of segments in the rule path. */ + int len; + + /* The array of path segments for this rule. Will be NULL for the + repository root. */ + authz_rule_segment_t *path; +} authz_rule_t; + + +/* An access control list defined by access rules. */ +typedef struct authz_acl_t +{ + /* The sequence number of the ACL stores the order in which access + rules were defined in the authz file. The authz lookup code + selects the highest-numbered ACL from amongst a set of equivalent + matches. */ + int sequence_number; + + /* The parsed rule. */ + authz_rule_t rule; + + /* Access rights for anonymous users */ + svn_boolean_t has_anon_access; + authz_access_t anon_access; + + /* Access rights for authenticated users */ + svn_boolean_t has_authn_access; + authz_access_t authn_access; + + /* All other user- or group-specific access rights. + Aliases are replaced with their definitions, rules for the same + user or group are merged. */ + apr_array_header_t *user_access; +} authz_acl_t; + + +/* An access control entry in authz_acl_t::user_access. */ +typedef struct authz_ace_t +{ + /* The name of the alias, user or group that this ACE applies to. */ + const char *name; + + /* The set of group members, when NAME is the name of a group. + We store this reference in the ACE to save a hash lookup when + resolving access for group ACEs. + */ + apr_hash_t *members; + + /* True if this is an inverse-match rule. */ + svn_boolean_t inverted; + + /* The access rights defined by this ACE. */ + authz_access_t access; +} authz_ace_t; + + +/* Parse authz definitions from RULES and optional global group + * definitions from GROUPS, returning an immutable, in-memory + * representation of all the rules, groups and aliases. + * + * **AUTHZ and its contents will be allocated from RESULT_POOL. + * The function uses SCRATCH_POOL for temporary allocations. + */ +svn_error_t * +svn_authz__parse(authz_full_t **authz, + svn_stream_t *rules, + svn_stream_t *groups, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Reverse a STRING of length LEN in place. */ +void +svn_authz__reverse_string(char *string, apr_size_t len); + + +/* Compare two rules in lexical order by path only. */ +int +svn_authz__compare_paths(const authz_rule_t *a, const authz_rule_t *b); + +/* Compare two rules in path lexical order, then repository lexical order. */ +int +svn_authz__compare_rules(const authz_rule_t *a, const authz_rule_t *b); + + +/* + * Authorization lookup + */ + +/* The "anonymous" user for authz queries. */ +#define AUTHZ_ANONYMOUS_USER ((const char*)"") + +/* Rules with this repository name apply to all repositories. */ +#define AUTHZ_ANY_REPOSITORY ((const char*)"") + +/* Check if the ACL applies to the REPOS pair. */ +svn_boolean_t +svn_authz__acl_applies_to_repo(const authz_acl_t *acl, + const char *repos); + +/* Check if the ACL applies to the (USER, REPOS) pair. If it does, + * and ACCESS is not NULL, set *ACCESS to the actual access rights for + * the user in this repository. + */ +svn_boolean_t +svn_authz__get_acl_access(authz_access_t *access, + const authz_acl_t *acl, + const char *user, const char *repos); + + +/* Set *RIGHTS to the accumulated global access rights calculated in + * AUTHZ for (USER, REPOS). + * Return TRUE if the rights are explicit (i.e., an ACL for REPOS + * applies to USER, or REPOS is AUTHZ_ANY_REPOSITORY). + */ +svn_boolean_t +svn_authz__get_global_rights(authz_rights_t *rights, + const authz_full_t *authz, + const char *user, const char *repos); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVN_REPOS_AUTHZ_H */ diff --git a/subversion/libsvn_repos/authz_info.c b/subversion/libsvn_repos/authz_info.c new file mode 100644 index 000000000000..8f3a8b63b12a --- /dev/null +++ b/subversion/libsvn_repos/authz_info.c @@ -0,0 +1,184 @@ +/* authz_info.c : Information derived from authz settings. + * + * ==================================================================== + * 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_hash.h> +#include <apr_pools.h> +#include <apr_tables.h> + +#include "svn_hash.h" + +#include "svn_private_config.h" + +#include "authz.h" + + +svn_boolean_t +svn_authz__acl_applies_to_repo(const authz_acl_t *acl, + const char *repos) +{ + /* The repository name must match the one in the rule, iff the rule + was defined for a specific repository. */ + return (0 == strcmp(acl->rule.repos, AUTHZ_ANY_REPOSITORY)) + || (0 == strcmp(repos, acl->rule.repos)); +} + +svn_boolean_t +svn_authz__get_acl_access(authz_access_t *access_p, + const authz_acl_t *acl, + const char *user, const char *repos) +{ + authz_access_t access; + svn_boolean_t has_access; + int i; + + /* The repository name must match the one in the rule, iff the rule + was defined for a specific repository. */ + if (!svn_authz__acl_applies_to_repo(acl, repos)) + return FALSE; + + /* Check anonymous access first. */ + if (!user || 0 == strcmp(user, AUTHZ_ANONYMOUS_USER)) + { + if (!acl->has_anon_access) + return FALSE; + + if (access_p) + *access_p = acl->anon_access; + return TRUE; + } + + /* Get the access rights for all authenticated users. */ + has_access = acl->has_authn_access; + access = (has_access ? acl->authn_access : authz_access_none); + + /* Scan the ACEs in the ACL and merge the access rights. */ + for (i = 0; i < acl->user_access->nelts; ++i) + { + const authz_ace_t *const ace = + &APR_ARRAY_IDX(acl->user_access, i, authz_ace_t); + const svn_boolean_t match = + ((ace->members && svn_hash_gets(ace->members, user)) + || (!ace->members && 0 == strcmp(user, ace->name))); + + if (!match != !ace->inverted) /* match XNOR ace->inverted */ + { + access |= ace->access; + has_access = TRUE; + } + } + + if (access_p) + *access_p = access; + return has_access; +} + +/* Set *RIGHTS_P to the combination of LHS and RHS, i.e. intersect the + * minimal rights and join the maximum rights. + */ +static void +combine_rights(authz_rights_t *rights_p, + const authz_rights_t *lhs, + const authz_rights_t *rhs) +{ + rights_p->min_access = lhs->min_access & rhs->min_access; + rights_p->max_access = lhs->max_access | rhs->max_access; +} + + +/* Given GLOBAL_RIGHTS and a repository name REPOS, set *RIGHTS_P to + * to the actual accumulated rights defined for that repository. + * Return TRUE if these rights were defined explicitly. + */ +static svn_boolean_t +resolve_global_rights(authz_rights_t *rights_p, + const authz_global_rights_t *global_rights, + const char *repos) +{ + if (0 == strcmp(repos, AUTHZ_ANY_REPOSITORY)) + { + /* Return the accumulated rights that are not repository-specific. */ + *rights_p = global_rights->any_repos_rights; + return TRUE; + } + else + { + /* Check if we have explicit rights for this repository. */ + const authz_rights_t *const rights = + svn_hash_gets(global_rights->per_repos_rights, repos); + + if (rights) + { + combine_rights(rights_p, rights, &global_rights->any_repos_rights); + return TRUE; + } + } + + /* Fall-through: return the rights defined for "any" repository + because this user has no specific rules for this specific REPOS. */ + *rights_p = global_rights->any_repos_rights; + return FALSE; +} + + +svn_boolean_t +svn_authz__get_global_rights(authz_rights_t *rights_p, + const authz_full_t *authz, + const char *user, const char *repos) +{ + if (!user || 0 == strcmp(user, AUTHZ_ANONYMOUS_USER)) + { + /* Check if we have explicit rights for anonymous access. */ + if (authz->has_anon_rights) + return resolve_global_rights(rights_p, &authz->anon_rights, repos); + } + else + { + /* Check if we have explicit rights for this user. */ + const authz_global_rights_t *const user_rights = + svn_hash_gets(authz->user_rights, user); + + if (user_rights) + { + svn_boolean_t explicit + = resolve_global_rights(rights_p, user_rights, repos); + + /* Rights given to _any_ authenticated user may apply, too. */ + if (authz->has_authn_rights) + { + authz_rights_t authn; + explicit |= resolve_global_rights(&authn, &authz->authn_rights, + repos); + combine_rights(rights_p, rights_p, &authn); + } + return explicit; + } + + /* Check if we have explicit rights for authenticated access. */ + if (authz->has_authn_rights) + return resolve_global_rights(rights_p, &authz->authn_rights, repos); + } + + /* Fall-through: return the implicit rights, i.e., none. */ + rights_p->min_access = authz_access_none; + rights_p->max_access = authz_access_none; + return FALSE; +} diff --git a/subversion/libsvn_repos/authz_parse.c b/subversion/libsvn_repos/authz_parse.c new file mode 100644 index 000000000000..23612dedd490 --- /dev/null +++ b/subversion/libsvn_repos/authz_parse.c @@ -0,0 +1,1442 @@ +/* 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 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"; + + +/* 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.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(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); + 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->parser_pool = parser_pool; + + insert_default_acl(cb); + + return cb; +} + + +/* 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.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); + } + } + + 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. */ +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); + } + 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; + 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 */ + members = svn_hash_gets(cb->parsed_groups, member); + if (!members) + return svn_error_createf( + SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, + _("Undefined group '%s'"), + member); + SVN_ERR(expand_group_callback(cb, key, klen, + 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); + } + + 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); + } + 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, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + ctor_baton_t *const cb = create_ctor_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; +} diff --git a/subversion/libsvn_repos/authz_pool.c b/subversion/libsvn_repos/authz_pool.c deleted file mode 100644 index f8ac52835876..000000000000 --- a/subversion/libsvn_repos/authz_pool.c +++ /dev/null @@ -1,226 +0,0 @@ -/* - * authz_pool.c : pool of authorization objects - * - * ==================================================================== - * 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 "svn_checksum.h" -#include "svn_config.h" -#include "svn_error.h" -#include "svn_pools.h" - -#include "private/svn_dep_compat.h" -#include "private/svn_mutex.h" -#include "private/svn_object_pool.h" -#include "private/svn_subr_private.h" -#include "private/svn_repos_private.h" -#include "private/svn_string_private.h" -#include "private/svn_subr_private.h" - -#include "repos.h" - -/* Currently this structure is just a wrapper around a svn_config_t. - */ -struct svn_authz_t -{ - svn_config_t *cfg; -}; - -/* The wrapper object structure that we store in the object pool. It - * combines the authz with the underlying config structures and their - * identifying keys. - */ -typedef struct authz_object_t -{ - /* key = concatenation of AUTHZ_KEY and GROUPS_KEY */ - svn_membuf_t *key; - - /* keys used to identify AUTHZ_CFG and GROUPS_CFG */ - svn_membuf_t *authz_key; - svn_membuf_t *groups_key; - - /* r/o references to configurations from the configuration pool. - GROUPS_CFG may be NULL. */ - svn_config_t *authz_cfg; - svn_config_t *groups_cfg; - - /* Case-sensitive config. */ - svn_authz_t *authz; -} authz_object_t; - -/* Root data structure simply adding the config_pool to the basic object pool. - */ -struct svn_repos__authz_pool_t -{ - /* authz_object_t object storage */ - svn_object_pool__t *object_pool; - - /* factory and storage of (shared) configuration objects */ - svn_repos__config_pool_t *config_pool; -}; - -/* Return a combination of AUTHZ_KEY and GROUPS_KEY, allocated in POOL. - * GROUPS_KEY may be NULL. - */ -static svn_membuf_t * -construct_key(svn_membuf_t *authz_key, - svn_membuf_t *groups_key, - apr_pool_t *pool) -{ - svn_membuf_t *result = apr_pcalloc(pool, sizeof(*result)); - apr_size_t size; - if (groups_key) - { - size = authz_key->size + groups_key->size; - svn_membuf__create(result,size, pool); - memcpy(result->data, authz_key->data, authz_key->size); - memcpy((char *)result->data + authz_key->size, - groups_key->data, groups_key->size); - } - else - { - size = authz_key->size; - svn_membuf__create(result, size, pool); - memcpy(result->data, authz_key->data, authz_key->size); - } - - result->size = size; - return result; -} - -/* Implement svn_object_pool__getter_t on authz_object_t structures. - */ -static void * -getter(void *object, - void *baton, - apr_pool_t *pool) -{ - return ((authz_object_t *)object)->authz; -} - -/* API implementation */ - -svn_error_t * -svn_repos__authz_pool_create(svn_repos__authz_pool_t **authz_pool, - svn_repos__config_pool_t *config_pool, - svn_boolean_t thread_safe, - apr_pool_t *pool) -{ - svn_repos__authz_pool_t *result; - svn_object_pool__t *object_pool; - - /* there is no setter as we don't need to update existing authz */ - SVN_ERR(svn_object_pool__create(&object_pool, getter, NULL, thread_safe, - pool)); - - result = apr_pcalloc(pool, sizeof(*result)); - result->object_pool = object_pool; - result->config_pool = config_pool; - - *authz_pool = result; - return SVN_NO_ERROR; -} - -svn_error_t * -svn_repos__authz_pool_get(svn_authz_t **authz_p, - svn_repos__authz_pool_t *authz_pool, - const char *path, - const char *groups_path, - svn_boolean_t must_exist, - svn_repos_t *preferred_repos, - apr_pool_t *pool) -{ - apr_pool_t *authz_ref_pool - = svn_object_pool__new_wrapper_pool(authz_pool->object_pool); - authz_object_t *authz_ref - = apr_pcalloc(authz_ref_pool, sizeof(*authz_ref)); - svn_boolean_t have_all_keys; - - /* read the configurations */ - SVN_ERR(svn_repos__config_pool_get(&authz_ref->authz_cfg, - &authz_ref->authz_key, - authz_pool->config_pool, - path, must_exist, TRUE, - preferred_repos, authz_ref_pool)); - have_all_keys = authz_ref->authz_key != NULL; - - if (groups_path) - { - SVN_ERR(svn_repos__config_pool_get(&authz_ref->groups_cfg, - &authz_ref->groups_key, - authz_pool->config_pool, - groups_path, must_exist, TRUE, - preferred_repos, authz_ref_pool)); - have_all_keys &= authz_ref->groups_key != NULL; - } - - /* fall back to standard implementation in case we don't have all the - * facts (i.e. keys). */ - if (!have_all_keys) - return svn_error_trace(svn_repos_authz_read2(authz_p, path, groups_path, - must_exist, pool)); - - /* all keys are known and lookup is unambigious. */ - authz_ref->key = construct_key(authz_ref->authz_key, - authz_ref->groups_key, - authz_ref_pool); - - SVN_ERR(svn_object_pool__lookup((void **)authz_p, authz_pool->object_pool, - authz_ref->key, NULL, pool)); - if (*authz_p) - { - svn_pool_destroy(authz_ref_pool); - return SVN_NO_ERROR; - } - - authz_ref->authz = apr_palloc(authz_ref_pool, sizeof(*authz_ref->authz)); - authz_ref->authz->cfg = authz_ref->authz_cfg; - - if (groups_path) - { - /* Easy out: we prohibit local groups in the authz file when global - groups are being used. */ - if (svn_config_has_section(authz_ref->authz->cfg, - SVN_CONFIG_SECTION_GROUPS)) - return svn_error_createf(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, - "Error reading authz file '%s' with " - "groups file '%s':" - "Authz file cannot contain any groups " - "when global groups are being used.", - path, groups_path); - - /* We simply need to add the [Groups] section to the authz config. - */ - svn_config__shallow_replace_section(authz_ref->authz->cfg, - authz_ref->groups_cfg, - SVN_CONFIG_SECTION_GROUPS); - } - - /* Make sure there are no errors in the configuration. */ - SVN_ERR(svn_repos__authz_validate(authz_ref->authz, authz_ref_pool)); - - SVN_ERR(svn_object_pool__insert((void **)authz_p, authz_pool->object_pool, - authz_ref->key, authz_ref, NULL, - authz_ref_pool, pool)); - - return SVN_NO_ERROR; -} diff --git a/subversion/libsvn_repos/commit.c b/subversion/libsvn_repos/commit.c index 1190acc29fc5..6ce4cc6f7ffd 100644 --- a/subversion/libsvn_repos/commit.c +++ b/subversion/libsvn_repos/commit.c @@ -124,6 +124,7 @@ struct dir_baton svn_revnum_t base_rev; /* the revision I'm based on */ svn_boolean_t was_copied; /* was this directory added with history? */ apr_pool_t *pool; /* my personal pool, in which I am allocated. */ + svn_boolean_t checked_write; /* TRUE after successfull write check */ }; @@ -131,6 +132,7 @@ struct file_baton { struct edit_baton *edit_baton; const char *path; /* the -absolute- path to this file in the fs */ + svn_boolean_t checked_write; /* TRUE after successfull write check */ }; @@ -171,6 +173,30 @@ out_of_date(const char *path, svn_node_kind_t kind) path); } +/* Perform an out of date check for base_rev against created rev, + and a sanity check of base_rev. */ +static svn_error_t * +check_out_of_date(struct edit_baton *eb, + const char *path, + svn_node_kind_t kind, + svn_revnum_t base_rev, + svn_revnum_t created_rev) +{ + if (base_rev < created_rev) + { + return out_of_date(path, kind); + } + else if (base_rev > created_rev) + { + if (base_rev > svn_fs_txn_base_revision(eb->txn)) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("No such revision %ld"), + base_rev); + } + + return SVN_NO_ERROR; +} + static svn_error_t * invoke_commit_cb(svn_commit_callback2_t commit_cb, @@ -184,15 +210,16 @@ invoke_commit_cb(svn_commit_callback2_t commit_cb, /* const */ svn_string_t *date; /* const */ svn_string_t *author; svn_commit_info_t *commit_info; + apr_hash_t *revprops; if (commit_cb == NULL) return SVN_NO_ERROR; - SVN_ERR(svn_fs_revision_prop(&date, fs, revision, SVN_PROP_REVISION_DATE, - scratch_pool)); - SVN_ERR(svn_fs_revision_prop(&author, fs, revision, - SVN_PROP_REVISION_AUTHOR, - scratch_pool)); + SVN_ERR(svn_fs_revision_proplist2(&revprops, fs, revision, + TRUE, scratch_pool, scratch_pool)); + + date = svn_hash_gets(revprops, SVN_PROP_REVISION_DATE); + author = svn_hash_gets(revprops, SVN_PROP_REVISION_AUTHOR); commit_info = svn_create_commit_info(scratch_pool); @@ -298,7 +325,6 @@ add_file_or_directory(const char *path, const char *fs_path; svn_fs_root_t *copy_root; svn_node_kind_t kind; - size_t repos_url_len; svn_repos_authz_access_t required; /* Copy requires recursive write access to the destination path @@ -320,14 +346,12 @@ add_file_or_directory(const char *path, /* For now, require that the url come from the same repository that this commit is operating on. */ copy_path = svn_path_uri_decode(copy_path, subpool); - repos_url_len = strlen(eb->repos_url_decoded); - if (strncmp(copy_path, eb->repos_url_decoded, repos_url_len) != 0) + fs_path = svn_cstring_skip_prefix(copy_path, eb->repos_url_decoded); + if (!fs_path) return svn_error_createf (SVN_ERR_FS_GENERAL, NULL, _("Source url '%s' is from different repository"), copy_path); - fs_path = apr_pstrdup(subpool, copy_path + repos_url_len); - /* Now use the "fs_path" as an absolute path within the repository to make the copy from. */ SVN_ERR(svn_fs_revision_root(©_root, eb->fs, @@ -364,14 +388,18 @@ add_file_or_directory(const char *path, /* Build a new child baton. */ if (is_dir) { - *return_baton = make_dir_baton(eb, pb, full_path, was_copied, - SVN_INVALID_REVNUM, pool); + struct dir_baton *new_db = make_dir_baton(eb, pb, full_path, was_copied, + SVN_INVALID_REVNUM, pool); + + new_db->checked_write = TRUE; /* Just created */ + *return_baton = new_db; } else { struct file_baton *new_fb = apr_pcalloc(pool, sizeof(*new_fb)); new_fb->edit_baton = eb; new_fb->path = full_path; + new_fb->checked_write = TRUE; /* Just created */ *return_baton = new_fb; } @@ -392,9 +420,9 @@ open_root(void *edit_baton, struct edit_baton *eb = edit_baton; svn_revnum_t youngest; - /* Ignore BASE_REVISION. We always build our transaction against - HEAD. However, we will keep it in our dir baton for out of - dateness checks. */ + /* We always build our transaction against HEAD. However, we will + sanity-check BASE_REVISION and keep it in our dir baton for out + of dateness checks. */ SVN_ERR(svn_fs_youngest_rev(&youngest, eb->fs, eb->pool)); if (base_revision > youngest) @@ -448,7 +476,6 @@ delete_entry(const char *path, struct dir_baton *parent = parent_baton; struct edit_baton *eb = parent->edit_baton; svn_node_kind_t kind; - svn_revnum_t cr_rev; svn_repos_authz_access_t required = svn_authz_write; const char *full_path; @@ -473,14 +500,18 @@ delete_entry(const char *path, /* Now, make sure we're deleting the node we *think* we're deleting, else return an out-of-dateness error. */ - SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, pool)); - if (SVN_IS_VALID_REVNUM(revision) && (revision < cr_rev)) - return svn_error_trace(out_of_date(full_path, kind)); + if (SVN_IS_VALID_REVNUM(revision)) + { + svn_revnum_t cr_rev; + + SVN_ERR(svn_fs_node_created_rev(&cr_rev, eb->txn_root, full_path, pool)); + SVN_ERR(check_out_of_date(eb, full_path, kind, revision, cr_rev)); + } /* This routine is a mindless wrapper. We call svn_fs_delete() because that will delete files and recursively delete directories. */ - return svn_fs_delete(eb->txn_root, full_path, pool); + return svn_error_trace(svn_fs_delete(eb->txn_root, full_path, pool)); } @@ -535,18 +566,23 @@ apply_textdelta(void *file_baton, void **handler_baton) { struct file_baton *fb = file_baton; + struct edit_baton *eb = fb->edit_baton; - /* Check for write authorization. */ - SVN_ERR(check_authz(fb->edit_baton, fb->path, - fb->edit_baton->txn_root, - svn_authz_write, pool)); + if (!fb->checked_write) + { + /* Check for write authorization. */ + SVN_ERR(check_authz(eb, fb->path, eb->txn_root, + svn_authz_write, pool)); + fb->checked_write = TRUE; + } - return svn_fs_apply_textdelta(handler, handler_baton, - fb->edit_baton->txn_root, - fb->path, - base_checksum, - NULL, - pool); + return svn_error_trace( + svn_fs_apply_textdelta(handler, handler_baton, + eb->txn_root, + fb->path, + base_checksum, + NULL, + pool)); } @@ -590,8 +626,9 @@ open_file(const char *path, /* If the node our caller has is an older revision number than the one in our transaction, return an out-of-dateness error. */ - if (SVN_IS_VALID_REVNUM(base_revision) && (base_revision < cr_rev)) - return svn_error_trace(out_of_date(full_path, svn_node_file)); + if (SVN_IS_VALID_REVNUM(base_revision)) + SVN_ERR(check_out_of_date(eb, full_path, svn_node_file, + base_revision, cr_rev)); /* Build a new file baton */ new_fb = apr_pcalloc(pool, sizeof(*new_fb)); @@ -616,9 +653,13 @@ change_file_prop(void *file_baton, struct file_baton *fb = file_baton; struct edit_baton *eb = fb->edit_baton; - /* Check for write authorization. */ - SVN_ERR(check_authz(eb, fb->path, eb->txn_root, - svn_authz_write, pool)); + if (!fb->checked_write) + { + /* Check for write authorization. */ + SVN_ERR(check_authz(eb, fb->path, eb->txn_root, + svn_authz_write, pool)); + fb->checked_write = TRUE; + } return svn_repos_fs_change_node_prop(eb->txn_root, fb->path, name, value, pool); @@ -663,19 +704,24 @@ change_dir_prop(void *dir_baton, struct edit_baton *eb = db->edit_baton; /* Check for write authorization. */ - SVN_ERR(check_authz(eb, db->path, eb->txn_root, - svn_authz_write, pool)); - - if (SVN_IS_VALID_REVNUM(db->base_rev)) + if (!db->checked_write) { - /* Subversion rule: propchanges can only happen on a directory - which is up-to-date. */ - svn_revnum_t created_rev; - SVN_ERR(svn_fs_node_created_rev(&created_rev, - eb->txn_root, db->path, pool)); - - if (db->base_rev < created_rev) - return svn_error_trace(out_of_date(db->path, svn_node_dir)); + SVN_ERR(check_authz(eb, db->path, eb->txn_root, + svn_authz_write, pool)); + + if (SVN_IS_VALID_REVNUM(db->base_rev)) + { + /* Subversion rule: propchanges can only happen on a directory + which is up-to-date. */ + svn_revnum_t created_rev; + SVN_ERR(svn_fs_node_created_rev(&created_rev, + eb->txn_root, db->path, pool)); + + SVN_ERR(check_out_of_date(eb, db->path, svn_node_dir, + db->base_rev, created_rev)); + } + + db->checked_write = TRUE; /* Skip on further prop changes */ } return svn_repos_fs_change_node_prop(eb->txn_root, db->path, diff --git a/subversion/libsvn_repos/compat.c b/subversion/libsvn_repos/compat.c new file mode 100644 index 000000000000..405b1c415b8d --- /dev/null +++ b/subversion/libsvn_repos/compat.c @@ -0,0 +1,179 @@ +/* + * compat.c: compatibility shims to adapt between different API versions. + * + * ==================================================================== + * 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 "svn_repos.h" +#include "svn_compat.h" +#include "svn_hash.h" +#include "svn_props.h" +#include "svn_pools.h" + +#include "svn_private_config.h" + +#include "repos.h" + +#include "private/svn_repos_private.h" +#include "private/svn_subr_private.h" + + + +/*** log4 -> log5 ***/ + +/* Baton type to be used with both log4 compatibility callbacks. + * For each revision, we collect the CHANGES and then pass them + * on to INNER. */ +typedef struct log_entry_receiver_baton_t +{ + /* Pool to use to allocate CHANGES and its entries. + * Gets cleared after each revision. */ + apr_pool_t *changes_pool; + + /* Path changes reported so far for the current revision. + * Will be NULL before the first item gets added and will be reset + * to NULL after the INNER callback has returned. */ + apr_hash_t *changes; + + /* User-provided callback to send the log entry to. */ + svn_log_entry_receiver_t inner; + void *inner_baton; +} log_entry_receiver_baton_t; + +/* Return the action character (see svn_log_changed_path2_t) for KIND. + * Returns 0 for invalid KINDs. */ +static char +path_change_kind_to_char(svn_fs_path_change_kind_t kind) +{ + const char symbol[] = "MADR"; + + if (kind < svn_fs_path_change_modify || kind > svn_fs_path_change_replace) + return 0; + + return symbol[kind]; +} + +/* Implement svn_repos_path_change_receiver_t. + * Convert CHANGE and add it to the CHANGES list in *BATON. */ +static svn_error_t * +log4_path_change_receiver(void *baton, + svn_repos_path_change_t *change, + apr_pool_t *scratch_pool) +{ + log_entry_receiver_baton_t *b = baton; + svn_log_changed_path2_t *change_copy; + const char *path = apr_pstrmemdup(b->changes_pool, change->path.data, + change->path.len); + + /* Create a deep copy of the temporary CHANGE struct. */ + change_copy = svn_log_changed_path2_create(b->changes_pool); + change_copy->action = path_change_kind_to_char(change->change_kind); + + if (change->copyfrom_path) + change_copy->copyfrom_path = apr_pstrdup(b->changes_pool, + change->copyfrom_path); + + change_copy->copyfrom_rev = change->copyfrom_rev; + change_copy->node_kind = change->node_kind; + change_copy->text_modified = change->text_mod ? svn_tristate_true + : svn_tristate_false; + change_copy->props_modified = change->prop_mod ? svn_tristate_true + : svn_tristate_false; + + /* Auto-create the CHANGES container (happens for each first change + * in any revison. */ + if (b->changes == NULL) + b->changes = svn_hash__make(b->changes_pool); + + /* Add change to per-revision collection. */ + apr_hash_set(b->changes, path, change->path.len, change_copy); + + return SVN_NO_ERROR; +} + +/* Implement svn_log_entry_receiver_t. + * Combine the data gathered in BATON for this revision and send it + * to the user-provided log4-compatible callback. */ +static svn_error_t * +log4_entry_receiver(void *baton, + svn_repos_log_entry_t *log_entry, + apr_pool_t *scratch_pool) +{ + log_entry_receiver_baton_t *b = baton; + svn_log_entry_t *entry = svn_log_entry_create(scratch_pool); + + /* Complete the ENTRY. */ + entry->changed_paths = b->changes; + entry->revision = log_entry->revision; + entry->revprops = log_entry->revprops; + entry->has_children = log_entry->has_children; + entry->changed_paths2 = b->changes; + entry->non_inheritable = log_entry->non_inheritable; + entry->subtractive_merge = log_entry->subtractive_merge; + + /* Invoke the log4-compatible callback. */ + SVN_ERR(b->inner(b->inner_baton, entry, scratch_pool)); + + /* Release per-revision data. */ + svn_pool_clear(b->changes_pool); + b->changes = NULL; + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos__get_logs_compat(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + int limit, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + svn_boolean_t include_merged_revisions, + const apr_array_header_t *revprops, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + apr_pool_t *changes_pool = svn_pool_create(pool); + + log_entry_receiver_baton_t baton; + baton.changes_pool = changes_pool; + baton.changes = NULL; + baton.inner = receiver; + baton.inner_baton = receiver_baton; + + SVN_ERR(svn_repos_get_logs5(repos, paths, start, end, limit, + strict_node_history, + include_merged_revisions, + revprops, + authz_read_func, authz_read_baton, + discover_changed_paths + ? log4_path_change_receiver + : NULL, + &baton, + log4_entry_receiver, &baton, + pool)); + + svn_pool_destroy(changes_pool); + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/config_file.c b/subversion/libsvn_repos/config_file.c new file mode 100644 index 000000000000..918727796eb7 --- /dev/null +++ b/subversion/libsvn_repos/config_file.c @@ -0,0 +1,386 @@ +/* + * config_file.c : efficiently read config files from disk or repo + * + * ==================================================================== + * 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 "svn_checksum.h" +#include "svn_path.h" +#include "svn_pools.h" + +#include "private/svn_subr_private.h" +#include "private/svn_repos_private.h" +#include "private/svn_config_private.h" + +#include "config_file.h" + +#include "svn_private_config.h" + + + +struct config_access_t +{ + /* The last repository that we found the requested URL in. May be NULL. */ + svn_repos_t *repos; + + /* Owning pool of this structure and is private to this structure. + * All objects with the lifetime of this access object will be allocated + * from this pool. */ + apr_pool_t *pool; +}; + + + +/* A stream object that gives access to a representation's content but + * delays accessing the repository data until the stream is first used. + * IOW, the stream object is cheap as long as it is not accessed. + */ +typedef struct presentation_stream_baton_t +{ + svn_fs_root_t *root; + const char *fs_path; + apr_pool_t *pool; + svn_stream_t *inner; +} presentation_stream_baton_t; + +static svn_error_t * +auto_open_inner_stream(presentation_stream_baton_t *b) +{ + if (!b->inner) + { + svn_filesize_t length; + svn_stream_t *stream; + svn_stringbuf_t *contents; + + SVN_ERR(svn_fs_file_length(&length, b->root, b->fs_path, b->pool)); + SVN_ERR(svn_fs_file_contents(&stream, b->root, b->fs_path, b->pool)); + SVN_ERR(svn_stringbuf_from_stream(&contents, stream, + (apr_size_t)length, b->pool)); + b->inner = svn_stream_from_stringbuf(contents, b->pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +read_handler_rep(void *baton, char *buffer, apr_size_t *len) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_read2(b->inner, buffer, len)); +} + +static svn_error_t * +mark_handler_rep(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_mark(b->inner, mark, pool)); +} + +static svn_error_t * +seek_handler_rep(void *baton, const svn_stream_mark_t *mark) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_seek(b->inner, mark)); +} + +static svn_error_t * +skip_handler_rep(void *baton, apr_size_t len) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_skip(b->inner, len)); +} + +static svn_error_t * +data_available_handler_rep(void *baton, svn_boolean_t *data_available) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_data_available(b->inner, data_available)); +} + +static svn_error_t * +readline_handler_rep(void *baton, + svn_stringbuf_t **stringbuf, + const char *eol, + svn_boolean_t *eof, + apr_pool_t *pool) +{ + presentation_stream_baton_t *b = baton; + SVN_ERR(auto_open_inner_stream(b)); + + return svn_error_trace(svn_stream_readline(b->inner, stringbuf, eol, eof, + pool)); +} + +/* Return a lazy access stream for FS_PATH under ROOT, allocated in POOL. */ +static svn_stream_t * +representation_stream(svn_fs_root_t *root, + const char *fs_path, + apr_pool_t *pool) +{ + svn_stream_t *stream; + presentation_stream_baton_t *baton; + + baton = apr_pcalloc(pool, sizeof(*baton)); + baton->root = root; + baton->fs_path = fs_path; + baton->pool = pool; + + stream = svn_stream_create(baton, pool); + svn_stream_set_read2(stream, read_handler_rep, read_handler_rep); + svn_stream_set_mark(stream, mark_handler_rep); + svn_stream_set_seek(stream, seek_handler_rep); + svn_stream_set_skip(stream, skip_handler_rep); + svn_stream_set_data_available(stream, data_available_handler_rep); + svn_stream_set_readline(stream, readline_handler_rep); + return stream; +} + +/* Handle the case of a file PATH / url pointing to anything that is either + * not a file or does not exist at all. The case is given by NODE_KIND. + * + * If MUST_EXIST is not set and the file does not exist at all, return a + * default *STREAM and *CHECKSUM allocated in the context of ACCESS, or an + * error otherwise. + */ +static svn_error_t * +handle_missing_file(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *path, + svn_boolean_t must_exist, + svn_node_kind_t node_kind) +{ + if (node_kind == svn_node_none && !must_exist) + { + *stream = svn_stream_empty(access->pool); + SVN_ERR(svn_checksum(checksum, svn_checksum_md5, "", 0, access->pool)); + } + else if (node_kind != svn_node_file) + { + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + "'%s' is not a file", path); + } + + return SVN_NO_ERROR; +} + +/* Open the in-repository file at URL, return its content checksum in + * *CHECKSUM and the content itself through *STREAM. Allocate those with + * the lifetime of ACCESS and use SCRATCH_POOL for temporaries. + * + * Error out when the file does not exist but MUST_EXIST is set. + */ +static svn_error_t * +get_repos_config(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *url, + svn_boolean_t must_exist, + apr_pool_t *scratch_pool) +{ + svn_fs_t *fs; + svn_fs_root_t *root; + svn_revnum_t youngest_rev; + svn_node_kind_t node_kind; + const char *dirent; + const char *fs_path; + const char *repos_root_dirent; + + SVN_ERR(svn_uri_get_dirent_from_file_url(&dirent, url, access->pool)); + + /* Maybe we can use the repos hint instance instead of creating a + * new one. */ + if (access->repos) + { + repos_root_dirent = svn_repos_path(access->repos, scratch_pool); + if (!svn_dirent_is_absolute(repos_root_dirent)) + SVN_ERR(svn_dirent_get_absolute(&repos_root_dirent, + repos_root_dirent, + scratch_pool)); + + if (!svn_dirent_is_ancestor(repos_root_dirent, dirent)) + access->repos = NULL; + } + + /* Open repos if no suitable repos is available. */ + if (!access->repos) + { + /* Search for a repository in the full path. */ + repos_root_dirent = svn_repos_find_root_path(dirent, scratch_pool); + + /* Attempt to open a repository at repos_root_dirent. */ + SVN_ERR(svn_repos_open3(&access->repos, repos_root_dirent, NULL, + access->pool, scratch_pool)); + } + + fs_path = &dirent[strlen(repos_root_dirent)]; + + /* Get the filesystem. */ + fs = svn_repos_fs(access->repos); + + /* Find HEAD and the revision root */ + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, scratch_pool)); + SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, access->pool)); + + /* Special case: non-existent paths may be handled as "empty" contents. */ + SVN_ERR(svn_fs_check_path(&node_kind, root, fs_path, scratch_pool)); + if (node_kind != svn_node_file) + return svn_error_trace(handle_missing_file(stream, checksum, access, + url, must_exist, node_kind)); + + /* Fetch checksum and see whether we already have a matching config */ + SVN_ERR(svn_fs_file_checksum(checksum, svn_checksum_md5, root, fs_path, + TRUE, access->pool)); + + *stream = representation_stream(root, fs_path, access->pool); + + return SVN_NO_ERROR; +} + +/* Open the file at PATH, return its content checksum in CHECKSUM and the + * content itself through *STREAM. Allocate those with the lifetime of + * ACCESS. + */ +static svn_error_t * +get_file_config(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *path, + svn_boolean_t must_exist, + apr_pool_t *scratch_pool) +{ + svn_stringbuf_t *contents; + svn_node_kind_t node_kind; + + /* Special case: non-existent paths may be handled as "empty" contents. */ + SVN_ERR(svn_io_check_path(path, &node_kind, scratch_pool)); + if (node_kind != svn_node_file) + return svn_error_trace(handle_missing_file(stream, checksum, access, + path, must_exist, node_kind)); + + /* Now, we should be able to read the file. */ + SVN_ERR(svn_stringbuf_from_file2(&contents, path, access->pool)); + + /* calculate MD5 over the whole file contents */ + SVN_ERR(svn_checksum(checksum, svn_checksum_md5, + contents->data, contents->len, access->pool)); + *stream = svn_stream_from_stringbuf(contents, access->pool); + + return SVN_NO_ERROR; +} + +/* Read the configuration from path, URL or registry sub-tree PATH, return + * its content checksum in CHECKSUM and the content itself through *STREAM. + * Allocate those with the lifetime of ACCESS. + */ +static svn_error_t * +get_generic_config(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *path, + svn_boolean_t must_exist, + apr_pool_t *scratch_pool) +{ + svn_stringbuf_t *contents = svn_stringbuf_create_empty(access->pool); + svn_config_t *config; + + /* Read the configuration and serialize it into CONTENTS. + * That copy can then be processed by the authz parser etc. */ + SVN_ERR(svn_config_read3(&config, path, must_exist, TRUE, TRUE, + scratch_pool)); + SVN_ERR(svn_config__write(svn_stream_from_stringbuf(contents, scratch_pool), + config, scratch_pool)); + + /* calculate MD5 over the whole file contents */ + SVN_ERR(svn_checksum(checksum, svn_checksum_md5, + contents->data, contents->len, access->pool)); + *stream = svn_stream_from_stringbuf(contents, access->pool); + + return SVN_NO_ERROR; +} + +config_access_t * +svn_repos__create_config_access(svn_repos_t *repos_hint, + apr_pool_t *result_pool) +{ + apr_pool_t *pool = svn_pool_create(result_pool); + config_access_t *result = apr_pcalloc(pool, sizeof(*result)); + + result->repos = repos_hint; + result->pool = pool; + + return result; +} + +void +svn_repos__destroy_config_access(config_access_t *access) +{ + svn_pool_destroy(access->pool); +} + +svn_error_t * +svn_repos__get_config(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *path, + svn_boolean_t must_exist, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + /* Directly access the config data. */ + if (svn_path_is_url(path)) + err = get_repos_config(stream, checksum, access, path, must_exist, + scratch_pool); + else + err = get_file_config(stream, checksum, access, path, must_exist, + scratch_pool); + + /* Fallback to indirect access using the generic config file parser. + * This is mainly used for registry support under Win32. */ + if (err) + { + svn_error_t *err2 = get_generic_config(stream, checksum, access, path, + must_exist, scratch_pool); + if (err2) + { + svn_error_clear(err2); + } + else + { + svn_error_clear(err); + err = SVN_NO_ERROR; + } + } + + return svn_error_trace(err); +} diff --git a/subversion/libsvn_repos/config_file.h b/subversion/libsvn_repos/config_file.h new file mode 100644 index 000000000000..7fce18f13608 --- /dev/null +++ b/subversion/libsvn_repos/config_file.h @@ -0,0 +1,74 @@ +/* config_file.h : authz parsing and searching, private to libsvn_repos + * + * ==================================================================== + * 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. + * ==================================================================== + */ + +#ifndef SVN_REPOS_CONFIG_FILE_H +#define SVN_REPOS_CONFIG_FILE_H + +#include <apr_hash.h> +#include <apr_pools.h> +#include <apr_tables.h> + +#include "svn_config.h" +#include "svn_error.h" +#include "svn_io.h" +#include "svn_repos.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + + +/* An opaque struct that helps making config data access resource efficient. */ +typedef struct config_access_t config_access_t; + +/* Return a new config access struct allocated in RESULT_POOL. + * Try to access REPOS_HINT first when resolving URLs; may be NULL. */ +config_access_t * +svn_repos__create_config_access(svn_repos_t *repos_hint, + apr_pool_t *result_pool); + +/* Release all resources allocated while using ACCESS. */ +void +svn_repos__destroy_config_access(config_access_t *access); + +/* Using ACCESS as a helper object, access the textual configuration at PATH, + * which may be an URL or a local path. Return content's checksum in + * *CHECKSUM and provide its content in *STREAM. + * + * The access will fail if the item does not exist and MUST_EXIST is set. + * The result has the same lifetime as ACCESS. Use SCRATCH_POOL for + * temporary allocations. + */ +svn_error_t * +svn_repos__get_config(svn_stream_t **stream, + svn_checksum_t **checksum, + config_access_t *access, + const char *path, + svn_boolean_t must_exist, + apr_pool_t *scratch_pool); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVN_REPOS_CONFIG_FILE_H */ diff --git a/subversion/libsvn_repos/config_pool.c b/subversion/libsvn_repos/config_pool.c index 164bd983c02b..acf4be34061e 100644 --- a/subversion/libsvn_repos/config_pool.c +++ b/subversion/libsvn_repos/config_pool.c @@ -25,110 +25,17 @@ #include "svn_checksum.h" -#include "svn_config.h" -#include "svn_error.h" -#include "svn_hash.h" #include "svn_path.h" #include "svn_pools.h" -#include "svn_repos.h" -#include "private/svn_dep_compat.h" -#include "private/svn_mutex.h" #include "private/svn_subr_private.h" #include "private/svn_repos_private.h" -#include "private/svn_object_pool.h" #include "svn_private_config.h" - -/* Our wrapper structure for parsed svn_config_t* instances. All data in - * CS_CFG and CI_CFG is expanded (to make it thread-safe) and considered - * read-only. - */ -typedef struct config_object_t -{ - /* UUID of the configuration contents. - * This is a SHA1 checksum of the parsed textual representation of CFG. */ - svn_checksum_t *key; - - /* Parsed and expanded configuration. At least one of the following - * must not be NULL. */ - - /* Case-sensitive config. May be NULL */ - svn_config_t *cs_cfg; - - /* Case-insensitive config. May be NULL */ - svn_config_t *ci_cfg; -} config_object_t; - - -/* Data structure used to short-circuit the repository access for configs - * read via URL. After reading such a config successfully, we store key - * repository information here and will validate it without actually opening - * the repository. - * - * As this is only an optimization and may create many entries in - * svn_repos__config_pool_t's IN_REPO_HASH_POOL index, we clean them up - * once in a while. - */ -typedef struct in_repo_config_t -{ - /* URL used to open the configuration */ - const char *url; - - /* Path of the repository that contained URL */ - const char *repo_root; - - /* Head revision of that repository when last read */ - svn_revnum_t revision; - - /* Contents checksum of the file stored under URL@REVISION */ - svn_checksum_t *key; -} in_repo_config_t; - - -/* Core data structure extending the encapsulated OBJECT_POOL. All access - * to it must be serialized using the OBJECT_POOL->MUTEX. - * - * To speed up URL@HEAD lookups, we maintain IN_REPO_CONFIGS as a secondary - * hash index. It maps URLs as provided by the caller onto in_repo_config_t - * instances. If that is still up-to-date, a further lookup into CONFIG - * may yield the desired configuration without the need to actually open - * the respective repository. - * - * Unused configurations that are kept in the IN_REPO_CONFIGS hash and may - * be cleaned up when the hash is about to grow. - */ -struct svn_repos__config_pool_t -{ - svn_object_pool__t *object_pool; - - /* URL -> in_repo_config_t* mapping. - * This is only a partial index and will get cleared regularly. */ - apr_hash_t *in_repo_configs; - - /* allocate the IN_REPO_CONFIGS index and in_repo_config_t here */ - apr_pool_t *in_repo_hash_pool; -}; +#include "config_file.h" -/* Return an automatic reference to the CFG member in CONFIG that will be - * released when POOL gets cleaned up. The case sensitivity flag in *BATON - * selects the desired option and section name matching mode. - */ -static void * -getter(void *object, - void *baton, - apr_pool_t *pool) -{ - config_object_t *wrapper = object; - svn_boolean_t *case_sensitive = baton; - svn_config_t *config = *case_sensitive ? wrapper->cs_cfg : wrapper->ci_cfg; - - /* we need to duplicate the root structure as it contains temp. buffers */ - return config ? svn_config__shallow_copy(config, pool) : NULL; -} - /* Return a memory buffer structure allocated in POOL and containing the * data from CHECKSUM. */ @@ -146,288 +53,43 @@ checksum_as_key(svn_checksum_t *checksum, return result; } -/* Copy the configuration from the wrapper in SOURCE to the wrapper in - * *TARGET with the case sensitivity flag in *BATON selecting the config - * to copy. This is usually done to add the missing case-(in)-sensitive - * variant. Since we must hold all data in *TARGET from the same POOL, - * a deep copy is required. - */ -static svn_error_t * -setter(void **target, - void *source, - void *baton, - apr_pool_t *pool) -{ - svn_boolean_t *case_sensitive = baton; - config_object_t *target_cfg = *(config_object_t **)target; - config_object_t *source_cfg = source; - - /* Maybe, we created a variant with different case sensitivity? */ - if (*case_sensitive && target_cfg->cs_cfg == NULL) - { - SVN_ERR(svn_config_dup(&target_cfg->cs_cfg, source_cfg->cs_cfg, pool)); - svn_config__set_read_only(target_cfg->cs_cfg, pool); - } - else if (!*case_sensitive && target_cfg->ci_cfg == NULL) - { - SVN_ERR(svn_config_dup(&target_cfg->ci_cfg, source_cfg->ci_cfg, pool)); - svn_config__set_read_only(target_cfg->ci_cfg, pool); - } - - return SVN_NO_ERROR; -} - -/* Set *CFG to the configuration passed in as text in CONTENTS and *KEY to - * the corresponding object pool key. If no such configuration exists in - * CONFIG_POOL, yet, parse CONTENTS and cache the result. CASE_SENSITIVE - * controls option and section name matching. +/* Set *CFG to the configuration serialized in STREAM and cache it in + * CONFIG_POOL under CHECKSUM. The configuration will only be parsed if + * we can't find it the CONFIG_POOL already. * * RESULT_POOL determines the lifetime of the returned reference and * SCRATCH_POOL is being used for temporary allocations. */ static svn_error_t * -auto_parse(svn_config_t **cfg, - svn_membuf_t **key, - svn_repos__config_pool_t *config_pool, - svn_stringbuf_t *contents, - svn_boolean_t case_sensitive, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) -{ - svn_checksum_t *checksum; - config_object_t *config_object; - apr_pool_t *cfg_pool; - - /* calculate SHA1 over the whole file contents */ - SVN_ERR(svn_stream_close - (svn_stream_checksummed2 - (svn_stream_from_stringbuf(contents, scratch_pool), - &checksum, NULL, svn_checksum_sha1, TRUE, scratch_pool))); - - /* return reference to suitable config object if that already exists */ - *key = checksum_as_key(checksum, result_pool); - SVN_ERR(svn_object_pool__lookup((void **)cfg, config_pool->object_pool, - *key, &case_sensitive, result_pool)); - if (*cfg) - return SVN_NO_ERROR; - - /* create a pool for the new config object and parse the data into it */ - cfg_pool = svn_object_pool__new_wrapper_pool(config_pool->object_pool); - - config_object = apr_pcalloc(cfg_pool, sizeof(*config_object)); - - SVN_ERR(svn_config_parse(case_sensitive ? &config_object->cs_cfg - : &config_object->ci_cfg, - svn_stream_from_stringbuf(contents, scratch_pool), - case_sensitive, case_sensitive, cfg_pool)); - - /* switch config data to r/o mode to guarantee thread-safe access */ - svn_config__set_read_only(case_sensitive ? config_object->cs_cfg - : config_object->ci_cfg, - cfg_pool); - - /* add config in pool, handle loads races and return the right config */ - SVN_ERR(svn_object_pool__insert((void **)cfg, config_pool->object_pool, - *key, config_object, &case_sensitive, - cfg_pool, result_pool)); - - return SVN_NO_ERROR; -} - -/* Store a URL@REVISION to CHECKSUM, REPOS_ROOT in CONFIG_POOL. - */ -static svn_error_t * -add_checksum(svn_repos__config_pool_t *config_pool, - const char *url, - const char *repos_root, - svn_revnum_t revision, - svn_checksum_t *checksum) +find_config(svn_config_t **cfg, + svn_repos__config_pool_t *config_pool, + svn_stream_t *stream, + svn_checksum_t *checksum, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) { - apr_size_t path_len = strlen(url); - apr_pool_t *pool = config_pool->in_repo_hash_pool; - in_repo_config_t *config = apr_hash_get(config_pool->in_repo_configs, - url, path_len); - if (config) - { - /* update the existing entry */ - memcpy((void *)config->key->digest, checksum->digest, - svn_checksum_size(checksum)); - config->revision = revision; - - /* duplicate the string only if necessary */ - if (strcmp(config->repo_root, repos_root)) - config->repo_root = apr_pstrdup(pool, repos_root); - } - else - { - /* insert a new entry. - * Limit memory consumption by cyclically clearing pool and hash. */ - if (2 * svn_object_pool__count(config_pool->object_pool) - < apr_hash_count(config_pool->in_repo_configs)) - { - svn_pool_clear(pool); - config_pool->in_repo_configs = svn_hash__make(pool); - } - - /* construct the new entry */ - config = apr_pcalloc(pool, sizeof(*config)); - config->key = svn_checksum_dup(checksum, pool); - config->url = apr_pstrmemdup(pool, url, path_len); - config->repo_root = apr_pstrdup(pool, repos_root); - config->revision = revision; + /* First, attempt the cache lookup. */ + svn_membuf_t *key = checksum_as_key(checksum, scratch_pool); + SVN_ERR(svn_object_pool__lookup((void **)cfg, config_pool, key, + result_pool)); - /* add to index */ - apr_hash_set(config_pool->in_repo_configs, url, path_len, config); - } - - return SVN_NO_ERROR; -} - -/* Set *CFG to the configuration stored in URL@HEAD and cache it in - * CONFIG_POOL. CASE_SENSITIVE controls - * option and section name matching. If PREFERRED_REPOS is given, - * use that if it also matches URL. - * - * RESULT_POOL determines the lifetime of the returned reference and - * SCRATCH_POOL is being used for temporary allocations. - */ -static svn_error_t * -find_repos_config(svn_config_t **cfg, - svn_membuf_t **key, - svn_repos__config_pool_t *config_pool, - const char *url, - svn_boolean_t case_sensitive, - svn_repos_t *preferred_repos, - apr_pool_t *result_pool, - apr_pool_t *scratch_pool) -{ - svn_repos_t *repos = NULL; - svn_fs_t *fs; - svn_fs_root_t *root; - svn_revnum_t youngest_rev; - svn_node_kind_t node_kind; - const char *dirent; - svn_stream_t *stream; - const char *fs_path; - const char *repos_root_dirent; - svn_checksum_t *checksum; - svn_stringbuf_t *contents; - - *cfg = NULL; - SVN_ERR(svn_uri_get_dirent_from_file_url(&dirent, url, scratch_pool)); - - /* maybe we can use the preferred repos instance instead of creating a - * new one */ - if (preferred_repos) - { - repos_root_dirent = svn_repos_path(preferred_repos, scratch_pool); - if (!svn_dirent_is_absolute(repos_root_dirent)) - SVN_ERR(svn_dirent_get_absolute(&repos_root_dirent, - repos_root_dirent, - scratch_pool)); - - if (svn_dirent_is_ancestor(repos_root_dirent, dirent)) - repos = preferred_repos; - } - - /* open repos if no suitable preferred repos was provided. */ - if (!repos) - { - /* Search for a repository in the full path. */ - repos_root_dirent = svn_repos_find_root_path(dirent, scratch_pool); - - /* Attempt to open a repository at repos_root_dirent. */ - SVN_ERR(svn_repos_open3(&repos, repos_root_dirent, NULL, - scratch_pool, scratch_pool)); - } - - fs_path = &dirent[strlen(repos_root_dirent)]; - - /* Get the filesystem. */ - fs = svn_repos_fs(repos); - - /* Find HEAD and the revision root */ - SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, scratch_pool)); - SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, scratch_pool)); - - /* Fetch checksum and see whether we already have a matching config */ - SVN_ERR(svn_fs_file_checksum(&checksum, svn_checksum_sha1, root, fs_path, - FALSE, scratch_pool)); - if (checksum) - { - *key = checksum_as_key(checksum, scratch_pool); - SVN_ERR(svn_object_pool__lookup((void **)cfg, config_pool->object_pool, - *key, &case_sensitive, result_pool)); - } - - /* not parsed, yet? */ + /* Not found? => parse and cache */ if (!*cfg) { - svn_filesize_t length; + svn_config_t *config; - /* fetch the file contents */ - SVN_ERR(svn_fs_check_path(&node_kind, root, fs_path, scratch_pool)); - if (node_kind != svn_node_file) - return SVN_NO_ERROR; + /* create a pool for the new config object and parse the data into it */ + apr_pool_t *cfg_pool = svn_object_pool__new_item_pool(config_pool); + SVN_ERR(svn_config_parse(&config, stream, FALSE, FALSE, cfg_pool)); - SVN_ERR(svn_fs_file_length(&length, root, fs_path, scratch_pool)); - SVN_ERR(svn_fs_file_contents(&stream, root, fs_path, scratch_pool)); - SVN_ERR(svn_stringbuf_from_stream(&contents, stream, - (apr_size_t)length, scratch_pool)); + /* switch config data to r/o mode to guarantee thread-safe access */ + svn_config__set_read_only(config, cfg_pool); - /* handle it like ordinary file contents and cache it */ - SVN_ERR(auto_parse(cfg, key, config_pool, contents, case_sensitive, - result_pool, scratch_pool)); + /* add config in pool, handle loads races and return the right config */ + SVN_ERR(svn_object_pool__insert((void **)cfg, config_pool, key, + config, cfg_pool, result_pool)); } - /* store the (path,rev) -> checksum mapping as well */ - if (*cfg && checksum) - SVN_MUTEX__WITH_LOCK(svn_object_pool__mutex(config_pool->object_pool), - add_checksum(config_pool, url, repos_root_dirent, - youngest_rev, checksum)); - - return SVN_NO_ERROR; -} - -/* Given the URL, search the CONFIG_POOL for an entry that maps it URL to - * a content checksum and is still up-to-date. If this could be found, - * return the object's *KEY. Use POOL for allocations. - * - * Requires external serialization on CONFIG_POOL. - * - * Note that this is only the URL(+rev) -> Checksum lookup and does not - * guarantee that there is actually a config object available for *KEY. - */ -static svn_error_t * -key_by_url(svn_membuf_t **key, - svn_repos__config_pool_t *config_pool, - const char *url, - apr_pool_t *pool) -{ - svn_error_t *err; - svn_stringbuf_t *contents; - apr_int64_t current; - - /* hash lookup url -> sha1 -> config */ - in_repo_config_t *config = svn_hash_gets(config_pool->in_repo_configs, url); - *key = NULL; - if (!config) - return SVN_NO_ERROR; - - /* found *some* reference to a configuration. - * Verify that it is still current. Will fail for BDB repos. */ - err = svn_stringbuf_from_file2(&contents, - svn_dirent_join(config->repo_root, - "db/current", pool), - pool); - if (!err) - err = svn_cstring_atoi64(¤t, contents->data); - - if (err) - svn_error_clear(err); - else if (current == config->revision) - *key = checksum_as_key(config->key, pool); - return SVN_NO_ERROR; } @@ -438,94 +100,49 @@ svn_repos__config_pool_create(svn_repos__config_pool_t **config_pool, svn_boolean_t thread_safe, apr_pool_t *pool) { - svn_repos__config_pool_t *result; - svn_object_pool__t *object_pool; - - SVN_ERR(svn_object_pool__create(&object_pool, getter, setter, - thread_safe, pool)); - - /* construct the config pool in our private ROOT_POOL to survive POOL - * cleanup and to prevent threading issues with the allocator */ - result = apr_pcalloc(pool, sizeof(*result)); - - result->object_pool = object_pool; - result->in_repo_hash_pool = svn_pool_create(pool); - result->in_repo_configs = svn_hash__make(result->in_repo_hash_pool); - - *config_pool = result; - return SVN_NO_ERROR; + return svn_error_trace(svn_object_pool__create(config_pool, + thread_safe, pool)); } svn_error_t * svn_repos__config_pool_get(svn_config_t **cfg, - svn_membuf_t **key, svn_repos__config_pool_t *config_pool, const char *path, svn_boolean_t must_exist, - svn_boolean_t case_sensitive, svn_repos_t *preferred_repos, apr_pool_t *pool) { svn_error_t *err = SVN_NO_ERROR; apr_pool_t *scratch_pool = svn_pool_create(pool); + config_access_t *access = svn_repos__create_config_access(preferred_repos, + scratch_pool); + svn_stream_t *stream; + svn_checksum_t *checksum; - /* make sure we always have a *KEY object */ - svn_membuf_t *local_key = NULL; - if (key == NULL) - key = &local_key; - else - *key = NULL; - - if (svn_path_is_url(path)) - { - /* Read config file from repository. - * Attempt a quick lookup first. */ - SVN_MUTEX__WITH_LOCK(svn_object_pool__mutex(config_pool->object_pool), - key_by_url(key, config_pool, path, pool)); - if (*key) - { - SVN_ERR(svn_object_pool__lookup((void **)cfg, - config_pool->object_pool, - *key, &case_sensitive, pool)); - if (*cfg) - { - svn_pool_destroy(scratch_pool); - return SVN_NO_ERROR; - } - } - - /* Read and cache the configuration. This may fail. */ - err = find_repos_config(cfg, key, config_pool, path, case_sensitive, - preferred_repos, pool, scratch_pool); - if (err || !*cfg) - { - /* let the standard implementation handle all the difficult cases */ - svn_error_clear(err); - err = svn_repos__retrieve_config(cfg, path, must_exist, - case_sensitive, pool); - } - } - else + *cfg = NULL; + err = svn_repos__get_config(&stream, &checksum, access, path, must_exist, + scratch_pool); + if (!err) + err = svn_error_quick_wrapf(find_config(cfg, config_pool, stream, + checksum, pool, scratch_pool), + "Error while parsing config file: '%s':", + path); + + /* Let the standard implementation handle all the difficult cases. + * Note that for in-repo configs, there are no further special cases to + * check for and deal with. */ + if (!*cfg && !svn_path_is_url(path)) { - /* Outside of repo file. Read it. */ - svn_stringbuf_t *contents; - err = svn_stringbuf_from_file2(&contents, path, scratch_pool); - if (err) - { - /* let the standard implementation handle all the difficult cases */ - svn_error_clear(err); - err = svn_config_read3(cfg, path, must_exist, case_sensitive, - case_sensitive, pool); - } - else - { - /* parsing and caching will always succeed */ - err = auto_parse(cfg, key, config_pool, contents, case_sensitive, - pool, scratch_pool); - } + svn_error_clear(err); + err = svn_config_read3(cfg, path, must_exist, FALSE, FALSE, pool); } + svn_repos__destroy_config_access(access); svn_pool_destroy(scratch_pool); - return err; + /* we need to duplicate the root structure as it contains temp. buffers */ + if (*cfg) + *cfg = svn_config__shallow_copy(*cfg, pool); + + return svn_error_trace(err); } diff --git a/subversion/libsvn_repos/delta.c b/subversion/libsvn_repos/delta.c index 3e28c70b8d0e..9b1448d91bfd 100644 --- a/subversion/libsvn_repos/delta.c +++ b/subversion/libsvn_repos/delta.c @@ -266,6 +266,13 @@ svn_repos_dir_delta2(svn_fs_root_t *src_root, _("Invalid editor anchoring; at least one of the " "input paths is not a directory and there was no source entry")); + /* Don't report / compare stale revprops. However, revprop changes that + * are made by a 3rd party outside this delta operation, may not be + * detected as per our visibility guarantees. Reset the revprop caches + * for both roots in case they belong to different svn_fs_t instances. */ + SVN_ERR(svn_fs_refresh_revision_props(svn_fs_root_fs(tgt_root), pool)); + SVN_ERR(svn_fs_refresh_revision_props(svn_fs_root_fs(src_root), pool)); + /* Set the global target revision if one can be determined. */ if (svn_fs_is_revision_root(tgt_root)) { @@ -491,8 +498,8 @@ delta_proplists(struct context *c, SVN_ERR(change_fn(c, object, SVN_PROP_ENTRY_COMMITTED_REV, cr_str, subpool)); - SVN_ERR(svn_fs_revision_proplist(&r_props, fs, committed_rev, - pool)); + SVN_ERR(svn_fs_revision_proplist2(&r_props, fs, committed_rev, + FALSE, pool, subpool)); /* Transmit the committed-date. */ committed_date = svn_hash_gets(r_props, SVN_PROP_REVISION_DATE); @@ -596,19 +603,6 @@ send_text_delta(struct context *c, } } -svn_error_t * -svn_repos__compare_files(svn_boolean_t *changed_p, - svn_fs_root_t *root1, - const char *path1, - svn_fs_root_t *root2, - const char *path2, - apr_pool_t *pool) -{ - return svn_error_trace(svn_fs_contents_different(changed_p, root1, path1, - root2, path2, pool)); -} - - /* Make the appropriate edits on FILE_BATON to change its contents and properties from those in SOURCE_PATH to those in TARGET_PATH. */ static svn_error_t * diff --git a/subversion/libsvn_repos/deprecated.c b/subversion/libsvn_repos/deprecated.c index fe9d1d22761f..f502623a20ea 100644 --- a/subversion/libsvn_repos/deprecated.c +++ b/subversion/libsvn_repos/deprecated.c @@ -29,12 +29,17 @@ #include "svn_repos.h" #include "svn_compat.h" #include "svn_hash.h" +#include "svn_path.h" #include "svn_props.h" +#include "svn_pools.h" #include "svn_private_config.h" #include "repos.h" +#include "private/svn_repos_private.h" +#include "private/svn_subr_private.h" + @@ -503,9 +508,72 @@ svn_repos_fs_get_locks(apr_hash_t **locks, authz_read_baton, pool)); } +static svn_error_t * +mergeinfo_receiver(const char *path, + svn_mergeinfo_t mergeinfo, + void *baton, + apr_pool_t *scratch_pool) +{ + svn_mergeinfo_catalog_t catalog = baton; + apr_pool_t *result_pool = apr_hash_pool_get(catalog); + apr_size_t len = strlen(path); + + apr_hash_set(catalog, + apr_pstrmemdup(result_pool, path, len), + len, + svn_mergeinfo_dup(mergeinfo, result_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_fs_get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo, + svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t rev, + svn_mergeinfo_inheritance_t inherit, + svn_boolean_t include_descendants, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *pool) +{ + svn_mergeinfo_catalog_t result_catalog = svn_hash__make(pool); + SVN_ERR(svn_repos_fs_get_mergeinfo2(repos, paths, rev, inherit, + include_descendants, + authz_read_func, authz_read_baton, + mergeinfo_receiver, result_catalog, + pool)); + *mergeinfo = result_catalog; + + return SVN_NO_ERROR; +} /*** From logs.c ***/ svn_error_t * +svn_repos_get_logs4(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t start, + svn_revnum_t end, + int limit, + svn_boolean_t discover_changed_paths, + svn_boolean_t strict_node_history, + svn_boolean_t include_merged_revisions, + const apr_array_header_t *revprops, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_log_entry_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + return svn_repos__get_logs_compat(repos, paths, start, end, limit, + discover_changed_paths, + strict_node_history, + include_merged_revisions, revprops, + authz_read_func, authz_read_baton, + receiver, receiver_baton, pool); +} + +svn_error_t * svn_repos_get_logs3(svn_repos_t *repos, const apr_array_header_t *paths, svn_revnum_t start, @@ -731,6 +799,34 @@ repos_notify_handler(void *baton, } } +svn_error_t * +svn_repos_dump_fs3(svn_repos_t *repos, + svn_stream_t *stream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t incremental, + svn_boolean_t use_deltas, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_error_trace(svn_repos_dump_fs4(repos, + stream, + start_rev, + end_rev, + incremental, + use_deltas, + TRUE, + TRUE, + notify_func, + notify_baton, + NULL, NULL, + cancel_func, + cancel_baton, + pool)); +} svn_error_t * svn_repos_dump_fs2(svn_repos_t *repos, @@ -806,6 +902,31 @@ svn_repos_verify_fs(svn_repos_t *repos, /*** From load.c ***/ svn_error_t * +svn_repos_load_fs5(svn_repos_t *repos, + svn_stream_t *dumpstream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_boolean_t use_pre_commit_hook, + svn_boolean_t use_post_commit_hook, + svn_boolean_t validate_props, + svn_boolean_t ignore_dates, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *pool) +{ + return svn_repos_load_fs6(repos, dumpstream, start_rev, end_rev, + uuid_action, parent_dir, + use_post_commit_hook, use_post_commit_hook, + validate_props, ignore_dates, FALSE, + notify_func, notify_baton, + cancel_func, cancel_baton, pool); +} + +svn_error_t * svn_repos_load_fs4(svn_repos_t *repos, svn_stream_t *dumpstream, svn_revnum_t start_rev, @@ -997,6 +1118,40 @@ svn_repos_load_fs(svn_repos_t *repos, } svn_error_t * +svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **parser, + void **parse_baton, + svn_repos_t *repos, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t use_history, + svn_boolean_t validate_props, + enum svn_repos_load_uuid uuid_action, + const char *parent_dir, + svn_boolean_t use_pre_commit_hook, + svn_boolean_t use_post_commit_hook, + svn_boolean_t ignore_dates, + svn_repos_notify_func_t notify_func, + void *notify_baton, + apr_pool_t *pool) +{ + SVN_ERR(svn_repos_get_fs_build_parser6(parser, parse_baton, + repos, + start_rev, end_rev, + use_history, + validate_props, + uuid_action, + parent_dir, + use_pre_commit_hook, + use_post_commit_hook, + ignore_dates, + FALSE /* normalize_props */, + notify_func, + notify_baton, + pool)); + return SVN_NO_ERROR; +} + +svn_error_t * svn_repos_get_fs_build_parser4(const svn_repos_parse_fns3_t **callbacks, void **parse_baton, svn_repos_t *repos, @@ -1118,9 +1273,30 @@ svn_repos_fs_begin_txn_for_update(svn_fs_txn_t **txn_p, /*** From authz.c ***/ svn_error_t * +svn_repos_authz_read2(svn_authz_t **authz_p, + const char *path, + const char *groups_path, + svn_boolean_t must_exist, + apr_pool_t *pool) +{ + apr_pool_t *scratch_pool = svn_pool_create(pool); + svn_error_t *err = svn_repos_authz_read3(authz_p, path, groups_path, + must_exist, NULL, + pool, scratch_pool); + svn_pool_destroy(scratch_pool); + + return svn_error_trace(err); +} + +svn_error_t * svn_repos_authz_read(svn_authz_t **authz_p, const char *file, svn_boolean_t must_exist, apr_pool_t *pool) { - return svn_repos__authz_read(authz_p, file, NULL, must_exist, - FALSE, pool); + /* Prevent accidental new features in existing API. */ + if (svn_path_is_url(file)) + return svn_error_createf(SVN_ERR_ILLEGAL_TARGET, NULL, + "'%s' is not a file name", file); + + return svn_error_trace(svn_repos_authz_read2(authz_p, file, NULL, + must_exist, pool)); } diff --git a/subversion/libsvn_repos/dump.c b/subversion/libsvn_repos/dump.c index 189d724cd2de..960bba5816f1 100644 --- a/subversion/libsvn_repos/dump.c +++ b/subversion/libsvn_repos/dump.c @@ -326,7 +326,7 @@ store_delta(apr_file_t **tempfile, svn_filesize_t *len, svn_fs_root_t *newroot, const char *newpath, apr_pool_t *pool) { svn_stream_t *temp_stream; - apr_off_t offset = 0; + apr_off_t offset; svn_txdelta_stream_t *delta_stream; svn_txdelta_window_handler_t wh; void *whb; @@ -346,7 +346,7 @@ store_delta(apr_file_t **tempfile, svn_filesize_t *len, SVN_ERR(svn_txdelta_send_txstream(delta_stream, wh, whb, pool)); /* Get the length of the temporary file and rewind it. */ - SVN_ERR(svn_io_file_seek(*tempfile, APR_CUR, &offset, pool)); + SVN_ERR(svn_io_file_get_offset(&offset, *tempfile, pool)); *len = offset; offset = 0; return svn_io_file_seek(*tempfile, APR_SET, &offset, pool); @@ -1922,50 +1922,90 @@ get_dump_editor(const svn_delta_editor_t **editor, /* Helper for svn_repos_dump_fs. - Write a revision record of REV in FS to writable STREAM, using POOL. + Write a revision record of REV in REPOS to writable STREAM, using POOL. + Dump revision properties as well if INCLUDE_REVPROPS has been set. + AUTHZ_FUNC and AUTHZ_BATON are passed directly to the repos layer. */ static svn_error_t * write_revision_record(svn_stream_t *stream, - svn_fs_t *fs, + svn_repos_t *repos, svn_revnum_t rev, + svn_boolean_t include_revprops, + svn_repos_authz_func_t authz_func, + void *authz_baton, apr_pool_t *pool) { apr_hash_t *props; apr_time_t timetemp; svn_string_t *datevalue; - SVN_ERR(svn_fs_revision_proplist(&props, fs, rev, pool)); - - /* Run revision date properties through the time conversion to - canonicalize them. */ - /* ### Remove this when it is no longer needed for sure. */ - datevalue = svn_hash_gets(props, SVN_PROP_REVISION_DATE); - if (datevalue) + if (include_revprops) { - SVN_ERR(svn_time_from_cstring(&timetemp, datevalue->data, pool)); - datevalue = svn_string_create(svn_time_to_cstring(timetemp, pool), - pool); - svn_hash_sets(props, SVN_PROP_REVISION_DATE, datevalue); + SVN_ERR(svn_repos_fs_revision_proplist(&props, repos, rev, + authz_func, authz_baton, pool)); + + /* Run revision date properties through the time conversion to + canonicalize them. */ + /* ### Remove this when it is no longer needed for sure. */ + datevalue = svn_hash_gets(props, SVN_PROP_REVISION_DATE); + if (datevalue) + { + SVN_ERR(svn_time_from_cstring(&timetemp, datevalue->data, pool)); + datevalue = svn_string_create(svn_time_to_cstring(timetemp, pool), + pool); + svn_hash_sets(props, SVN_PROP_REVISION_DATE, datevalue); + } + } + else + { + /* Although we won't use it, we still need this container for the + call below. */ + props = apr_hash_make(pool); } SVN_ERR(svn_repos__dump_revision_record(stream, rev, NULL, props, - TRUE /*props_section_always*/, + include_revprops, pool)); return SVN_NO_ERROR; } +/* Baton for dump_filter_authz_func(). */ +typedef struct dump_filter_baton_t +{ + svn_repos_dump_filter_func_t filter_func; + void *filter_baton; +} dump_filter_baton_t; + +/* Implements svn_repos_authz_func_t. */ +static svn_error_t * +dump_filter_authz_func(svn_boolean_t *allowed, + svn_fs_root_t *root, + const char *path, + void *baton, + apr_pool_t *pool) +{ + dump_filter_baton_t *b = baton; + + return svn_error_trace(b->filter_func(allowed, root, path, b->filter_baton, + pool)); +} + /* The main dumper. */ svn_error_t * -svn_repos_dump_fs3(svn_repos_t *repos, +svn_repos_dump_fs4(svn_repos_t *repos, svn_stream_t *stream, svn_revnum_t start_rev, svn_revnum_t end_rev, svn_boolean_t incremental, svn_boolean_t use_deltas, + svn_boolean_t include_revprops, + svn_boolean_t include_changes, svn_repos_notify_func_t notify_func, void *notify_baton, + svn_repos_dump_filter_func_t filter_func, + void *filter_baton, svn_cancel_func_t cancel_func, void *cancel_baton, apr_pool_t *pool) @@ -1974,13 +2014,19 @@ svn_repos_dump_fs3(svn_repos_t *repos, void *dump_edit_baton = NULL; svn_revnum_t rev; svn_fs_t *fs = svn_repos_fs(repos); - apr_pool_t *subpool = svn_pool_create(pool); + apr_pool_t *iterpool = svn_pool_create(pool); svn_revnum_t youngest; const char *uuid; int version; svn_boolean_t found_old_reference = FALSE; svn_boolean_t found_old_mergeinfo = FALSE; svn_repos_notify_t *notify; + svn_repos_authz_func_t authz_func; + dump_filter_baton_t authz_baton = {0}; + + /* Make sure we catch up on the latest revprop changes. This is the only + * time we will refresh the revprop data in this query. */ + SVN_ERR(svn_fs_refresh_revision_props(fs, pool)); /* Determine the current youngest revision of the filesystem. */ SVN_ERR(svn_fs_youngest_rev(&youngest, fs, pool)); @@ -2005,6 +2051,20 @@ svn_repos_dump_fs3(svn_repos_t *repos, "(youngest revision is %ld)"), end_rev, youngest); + /* We use read authz callback to implement dump filtering. If there is no + * read access for some node, it will be excluded from dump as well as + * references to it (e.g. copy source). */ + if (filter_func) + { + authz_func = dump_filter_authz_func; + authz_baton.filter_func = filter_func; + authz_baton.filter_baton = filter_baton; + } + else + { + authz_func = NULL; + } + /* Write out the UUID. */ SVN_ERR(svn_fs_get_uuid(fs, &uuid, pool)); @@ -2033,18 +2093,20 @@ svn_repos_dump_fs3(svn_repos_t *repos, svn_fs_root_t *to_root; svn_boolean_t use_deltas_for_rev; - svn_pool_clear(subpool); + svn_pool_clear(iterpool); /* Check for cancellation. */ if (cancel_func) SVN_ERR(cancel_func(cancel_baton)); /* Write the revision record. */ - SVN_ERR(write_revision_record(stream, fs, rev, subpool)); + SVN_ERR(write_revision_record(stream, repos, rev, include_revprops, + authz_func, &authz_baton, iterpool)); /* When dumping revision 0, we just write out the revision record. - The parser might want to use its properties. */ - if (rev == 0) + The parser might want to use its properties. + If we don't want revision changes at all, skip in any case. */ + if (rev == 0 || !include_changes) goto loop_end; /* Fetch the editor which dumps nodes to a file. Regardless of @@ -2056,10 +2118,10 @@ svn_repos_dump_fs3(svn_repos_t *repos, &found_old_mergeinfo, NULL, notify_func, notify_baton, start_rev, use_deltas_for_rev, FALSE, FALSE, - subpool)); + iterpool)); /* Drive the editor in one way or another. */ - SVN_ERR(svn_fs_revision_root(&to_root, fs, rev, subpool)); + SVN_ERR(svn_fs_revision_root(&to_root, fs, rev, iterpool)); /* If this is the first revision of a non-incremental dump, we're in for a full tree dump. Otherwise, we want to simply @@ -2068,35 +2130,34 @@ svn_repos_dump_fs3(svn_repos_t *repos, { /* Compare against revision 0, so everything appears to be added. */ svn_fs_root_t *from_root; - SVN_ERR(svn_fs_revision_root(&from_root, fs, 0, subpool)); + SVN_ERR(svn_fs_revision_root(&from_root, fs, 0, iterpool)); SVN_ERR(svn_repos_dir_delta2(from_root, "", "", to_root, "", dump_editor, dump_edit_baton, - NULL, - NULL, + authz_func, &authz_baton, FALSE, /* don't send text-deltas */ svn_depth_infinity, FALSE, /* don't send entry props */ FALSE, /* don't ignore ancestry */ - subpool)); + iterpool)); } else { /* The normal case: compare consecutive revs. */ SVN_ERR(svn_repos_replay2(to_root, "", SVN_INVALID_REVNUM, FALSE, dump_editor, dump_edit_baton, - NULL, NULL, subpool)); + authz_func, &authz_baton, iterpool)); /* While our editor close_edit implementation is a no-op, we still do this for completeness. */ - SVN_ERR(dump_editor->close_edit(dump_edit_baton, subpool)); + SVN_ERR(dump_editor->close_edit(dump_edit_baton, iterpool)); } loop_end: if (notify_func) { notify->revision = rev; - notify_func(notify_baton, notify, subpool); + notify_func(notify_baton, notify, iterpool); } } @@ -2107,12 +2168,12 @@ svn_repos_dump_fs3(svn_repos_t *repos, warning, since the inline warnings already issued might easily be missed. */ - notify = svn_repos_notify_create(svn_repos_notify_dump_end, subpool); - notify_func(notify_baton, notify, subpool); + notify = svn_repos_notify_create(svn_repos_notify_dump_end, iterpool); + notify_func(notify_baton, notify, iterpool); if (found_old_reference) { - notify_warning(subpool, notify_func, notify_baton, + notify_warning(iterpool, notify_func, notify_baton, svn_repos_notify_warning_found_old_reference, _("The range of revisions dumped " "contained references to " @@ -2124,7 +2185,7 @@ svn_repos_dump_fs3(svn_repos_t *repos, in dumped mergeinfo. */ if (found_old_mergeinfo) { - notify_warning(subpool, notify_func, notify_baton, + notify_warning(iterpool, notify_func, notify_baton, svn_repos_notify_warning_found_old_mergeinfo, _("The range of revisions dumped " "contained mergeinfo " @@ -2133,7 +2194,7 @@ svn_repos_dump_fs3(svn_repos_t *repos, } } - svn_pool_destroy(subpool); + svn_pool_destroy(iterpool); return SVN_NO_ERROR; } @@ -2314,7 +2375,8 @@ verify_one_revision(svn_fs_t *fs, do this for completeness. */ SVN_ERR(cancel_editor->close_edit(cancel_edit_baton, scratch_pool)); - SVN_ERR(svn_fs_revision_proplist(&props, fs, rev, scratch_pool)); + SVN_ERR(svn_fs_revision_proplist2(&props, fs, rev, FALSE, scratch_pool, + scratch_pool)); return SVN_NO_ERROR; } @@ -2394,6 +2456,10 @@ svn_repos_verify_fs3(svn_repos_t *repos, struct verify_fs_notify_func_baton_t *verify_notify_baton = NULL; svn_error_t *err; + /* Make sure we catch up on the latest revprop changes. This is the only + * time we will refresh the revprop data in this query. */ + SVN_ERR(svn_fs_refresh_revision_props(fs, pool)); + /* Determine the current youngest revision of the filesystem. */ SVN_ERR(svn_fs_youngest_rev(&youngest, fs, pool)); diff --git a/subversion/libsvn_repos/fs-wrap.c b/subversion/libsvn_repos/fs-wrap.c index b46cda650fc2..f895d5aa0c22 100644 --- a/subversion/libsvn_repos/fs-wrap.c +++ b/subversion/libsvn_repos/fs-wrap.c @@ -33,6 +33,7 @@ #include "svn_repos.h" #include "svn_time.h" #include "svn_sorts.h" +#include "svn_subst.h" #include "repos.h" #include "svn_private_config.h" #include "private/svn_repos_private.h" @@ -58,6 +59,8 @@ svn_repos_fs_commit_txn(const char **conflict_p, apr_hash_t *hooks_env; *new_rev = SVN_INVALID_REVNUM; + if (conflict_p) + *conflict_p = NULL; /* Parse the hooks-env file (if any). */ SVN_ERR(svn_repos__parse_hooks_env(&hooks_env, repos->hooks_env_path, @@ -233,10 +236,13 @@ svn_repos__validate_prop(const char *name, * carriage return characters ('\r'). */ if (strchr(value->data, '\r') != NULL) { - return svn_error_createf - (SVN_ERR_BAD_PROPERTY_VALUE, NULL, + svn_error_t *err = svn_error_createf + (SVN_ERR_BAD_PROPERTY_VALUE_EOL, NULL, _("Cannot accept non-LF line endings in '%s' property"), name); + + return svn_error_create(SVN_ERR_BAD_PROPERTY_VALUE, err, + _("Invalid property value")); } } @@ -257,6 +263,34 @@ svn_repos__validate_prop(const char *name, } +svn_error_t * +svn_repos__normalize_prop(const svn_string_t **result_p, + svn_boolean_t *normalized_p, + const char *name, + const svn_string_t *value, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (svn_prop_needs_translation(name) && value) + { + svn_string_t *new_value; + + SVN_ERR(svn_subst_translate_string2(&new_value, NULL, normalized_p, + value, "UTF-8", TRUE, + result_pool, scratch_pool)); + *result_p = new_value; + } + else + { + *result_p = svn_string_dup(value, result_pool); + if (normalized_p) + *normalized_p = FALSE; + } + + return SVN_NO_ERROR; +} + + /* Verify the mergeinfo property value VALUE and return an error if it * is invalid. The PATH on which that property is set is used for error * messages only. Use SCRATCH_POOL for temporary allocations. */ @@ -378,7 +412,8 @@ svn_repos_fs_change_rev_prop4(svn_repos_t *repos, * to the hooks to be accurate. */ svn_string_t *old_value2; - SVN_ERR(svn_fs_revision_prop(&old_value2, repos->fs, rev, name, pool)); + SVN_ERR(svn_fs_revision_prop2(&old_value2, repos->fs, rev, name, + TRUE, pool, pool)); old_value = old_value2; } @@ -448,12 +483,13 @@ svn_repos_fs_revision_prop(svn_string_t **value_p, *value_p = NULL; else - SVN_ERR(svn_fs_revision_prop(value_p, repos->fs, - rev, propname, pool)); + SVN_ERR(svn_fs_revision_prop2(value_p, repos->fs, + rev, propname, TRUE, pool, pool)); } else /* wholly readable revision */ { - SVN_ERR(svn_fs_revision_prop(value_p, repos->fs, rev, propname, pool)); + SVN_ERR(svn_fs_revision_prop2(value_p, repos->fs, rev, propname, TRUE, + pool, pool)); } return SVN_NO_ERROR; @@ -486,7 +522,8 @@ svn_repos_fs_revision_proplist(apr_hash_t **table_p, svn_string_t *value; /* Produce two property hashtables, both in POOL. */ - SVN_ERR(svn_fs_revision_proplist(&tmphash, repos->fs, rev, pool)); + SVN_ERR(svn_fs_revision_proplist2(&tmphash, repos->fs, rev, TRUE, + pool, pool)); *table_p = apr_hash_make(pool); /* If they exist, we only copy svn:author and svn:date into the @@ -501,7 +538,8 @@ svn_repos_fs_revision_proplist(apr_hash_t **table_p, } else /* wholly readable revision */ { - SVN_ERR(svn_fs_revision_proplist(table_p, repos->fs, rev, pool)); + SVN_ERR(svn_fs_revision_proplist2(table_p, repos->fs, rev, TRUE, + pool, pool)); } return SVN_NO_ERROR; @@ -895,25 +933,26 @@ svn_repos_fs_get_locks2(apr_hash_t **locks, svn_error_t * -svn_repos_fs_get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo, - svn_repos_t *repos, - const apr_array_header_t *paths, - svn_revnum_t rev, - svn_mergeinfo_inheritance_t inherit, - svn_boolean_t include_descendants, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, - apr_pool_t *pool) +svn_repos_fs_get_mergeinfo2(svn_repos_t *repos, + const apr_array_header_t *paths, + svn_revnum_t rev, + svn_mergeinfo_inheritance_t inherit, + svn_boolean_t include_descendants, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_repos_mergeinfo_receiver_t receiver, + void *receiver_baton, + apr_pool_t *scratch_pool) { /* Here we cast away 'const', but won't try to write through this pointer * without first allocating a new array. */ apr_array_header_t *readable_paths = (apr_array_header_t *) paths; svn_fs_root_t *root; - apr_pool_t *iterpool = svn_pool_create(pool); + apr_pool_t *iterpool = svn_pool_create(scratch_pool); if (!SVN_IS_VALID_REVNUM(rev)) - SVN_ERR(svn_fs_youngest_rev(&rev, repos->fs, pool)); - SVN_ERR(svn_fs_revision_root(&root, repos->fs, rev, pool)); + SVN_ERR(svn_fs_youngest_rev(&rev, repos->fs, scratch_pool)); + SVN_ERR(svn_fs_revision_root(&root, repos->fs, rev, scratch_pool)); /* Filter out unreadable paths before divining merge tracking info. */ if (authz_read_func) @@ -934,7 +973,7 @@ svn_repos_fs_get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo, /* Requested paths differ from readable paths. Fork list of readable paths from requested paths. */ int j; - readable_paths = apr_array_make(pool, paths->nelts - 1, + readable_paths = apr_array_make(scratch_pool, paths->nelts - 1, sizeof(char *)); for (j = 0; j < i; j++) { @@ -951,10 +990,10 @@ svn_repos_fs_get_mergeinfo(svn_mergeinfo_catalog_t *mergeinfo, the change itself. */ /* ### TODO(reint): ... but how about descendant merged-to paths? */ if (readable_paths->nelts > 0) - SVN_ERR(svn_fs_get_mergeinfo2(mergeinfo, root, readable_paths, inherit, - include_descendants, TRUE, pool, pool)); - else - *mergeinfo = apr_hash_make(pool); + SVN_ERR(svn_fs_get_mergeinfo3(root, readable_paths, inherit, + include_descendants, TRUE, + receiver, receiver_baton, + scratch_pool)); svn_pool_destroy(iterpool); return SVN_NO_ERROR; @@ -975,15 +1014,18 @@ pack_notify_func(void *baton, { struct pack_notify_baton *pnb = baton; svn_repos_notify_t *notify; + svn_repos_notify_action_t repos_action; /* Simple conversion works for these values. */ SVN_ERR_ASSERT(pack_action >= svn_fs_pack_notify_start - && pack_action <= svn_fs_pack_notify_end_revprop); + && pack_action <= svn_fs_pack_notify_noop); + + repos_action = pack_action == svn_fs_pack_notify_noop + ? svn_repos_notify_pack_noop + : pack_action + svn_repos_notify_pack_shard_start + - svn_fs_pack_notify_start; - notify = svn_repos_notify_create(pack_action - + svn_repos_notify_pack_shard_start - - svn_fs_pack_notify_start, - pool); + notify = svn_repos_notify_create(repos_action, pool); notify->shard = shard; pnb->notify_func(pnb->notify_baton, notify, pool); diff --git a/subversion/libsvn_repos/hooks.c b/subversion/libsvn_repos/hooks.c index a4cc2491416f..8d17a82c31bd 100644 --- a/subversion/libsvn_repos/hooks.c +++ b/subversion/libsvn_repos/hooks.c @@ -476,11 +476,8 @@ svn_repos__hooks_start_commit(svn_repos_t *repos, if (capabilities) { - capabilities_string = svn_cstring_join(capabilities, ":", pool); - - /* Get rid of that annoying final colon. */ - if (capabilities_string[0]) - capabilities_string[strlen(capabilities_string) - 1] = '\0'; + capabilities_string = svn_cstring_join2(capabilities, ":", + FALSE, pool); } else { @@ -799,8 +796,8 @@ svn_repos__hooks_post_lock(svn_repos_t *repos, { const char *args[5]; apr_file_t *stdin_handle = NULL; - svn_string_t *paths_str = svn_string_create(svn_cstring_join - (paths, "\n", pool), + svn_string_t *paths_str = svn_string_create(svn_cstring_join2 + (paths, "\n", TRUE, pool), pool); SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool)); @@ -875,8 +872,8 @@ svn_repos__hooks_post_unlock(svn_repos_t *repos, { const char *args[5]; apr_file_t *stdin_handle = NULL; - svn_string_t *paths_str = svn_string_create(svn_cstring_join - (paths, "\n", pool), + svn_string_t *paths_str = svn_string_create(svn_cstring_join2 + (paths, "\n", TRUE, pool), pool); SVN_ERR(create_temp_file(&stdin_handle, paths_str, pool)); diff --git a/subversion/libsvn_repos/list.c b/subversion/libsvn_repos/list.c new file mode 100644 index 000000000000..ef8ac32b1b38 --- /dev/null +++ b/subversion/libsvn_repos/list.c @@ -0,0 +1,340 @@ +/* list.c : listing repository contents + * + * ==================================================================== + * 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_pools.h> +#include <apr_fnmatch.h> + +#include "svn_pools.h" +#include "svn_error.h" +#include "svn_dirent_uri.h" +#include "svn_time.h" + +#include "private/svn_repos_private.h" +#include "private/svn_sorts_private.h" +#include "private/svn_utf_private.h" +#include "svn_private_config.h" /* for SVN_TEMPLATE_ROOT_DIR */ + +#include "repos.h" + + + +/* Utility function. Given DIRENT->KIND, set all other elements of *DIRENT + * with the values retrieved for PATH under ROOT. Allocate them in POOL. + */ +static svn_error_t * +fill_dirent(svn_dirent_t *dirent, + svn_fs_root_t *root, + const char *path, + apr_pool_t *scratch_pool) +{ + const char *datestring; + + if (dirent->kind == svn_node_file) + SVN_ERR(svn_fs_file_length(&(dirent->size), root, path, scratch_pool)); + else + dirent->size = SVN_INVALID_FILESIZE; + + SVN_ERR(svn_fs_node_has_props(&dirent->has_props, root, path, + scratch_pool)); + + SVN_ERR(svn_repos_get_committed_info(&(dirent->created_rev), + &datestring, + &(dirent->last_author), + root, path, scratch_pool)); + if (datestring) + SVN_ERR(svn_time_from_cstring(&(dirent->time), datestring, + scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_stat(svn_dirent_t **dirent, + svn_fs_root_t *root, + const char *path, + apr_pool_t *pool) +{ + svn_node_kind_t kind; + svn_dirent_t *ent; + + SVN_ERR(svn_fs_check_path(&kind, root, path, pool)); + + if (kind == svn_node_none) + { + *dirent = NULL; + return SVN_NO_ERROR; + } + + ent = svn_dirent_create(pool); + ent->kind = kind; + + SVN_ERR(fill_dirent(ent, root, path, pool)); + + *dirent = ent; + return SVN_NO_ERROR; +} + +/* Return TRUE of DIRNAME matches any of the const char * in PATTERNS. + * Note that any DIRNAME will match if PATTERNS is empty. + * Use SCRATCH_BUFFER for temporary string contents. */ +static svn_boolean_t +matches_any(const char *dirname, + const apr_array_header_t *patterns, + svn_membuf_t *scratch_buffer) +{ + return patterns + ? svn_utf__fuzzy_glob_match(dirname, patterns, scratch_buffer) + : TRUE; +} + +/* Utility to prevent code duplication. + * + * Construct a svn_dirent_t for PATH of type KIND under ROOT and, if + * PATH_INFO_ONLY is not set, fill it. Call RECEIVER with the result + * and RECEIVER_BATON. + * + * Use SCRATCH_POOL for temporary allocations. + */ +static svn_error_t * +report_dirent(svn_fs_root_t *root, + const char *path, + svn_node_kind_t kind, + svn_boolean_t path_info_only, + svn_repos_dirent_receiver_t receiver, + void *receiver_baton, + apr_pool_t *scratch_pool) +{ + svn_dirent_t dirent = { 0 }; + + /* Fetch the details to report - if required. */ + dirent.kind = kind; + if (!path_info_only) + SVN_ERR(fill_dirent(&dirent, root, path, scratch_pool)); + + /* Report the entry. */ + SVN_ERR(receiver(path, &dirent, receiver_baton, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Utility data struct, used to attach a filter result flag to a dirent. */ +typedef struct filtered_dirent_t +{ + /* Actual dirent, never NULL. */ + svn_fs_dirent_t *dirent; + + /* DIRENT passed the filter. */ + svn_boolean_t is_match; +} filtered_dirent_t; + +/* Implement a standard sort function for filtered_dirent_t *, sorting them + * by entry name. */ +static int +compare_filtered_dirent(const void *lhs, + const void *rhs) +{ + const filtered_dirent_t *lhs_dirent = (const filtered_dirent_t *)lhs; + const filtered_dirent_t *rhs_dirent = (const filtered_dirent_t *)rhs; + + return strcmp(lhs_dirent->dirent->name, rhs_dirent->dirent->name); +} + +/* Core of svn_repos_list with the same parameter list. + * + * However, DEPTH is not svn_depth_empty and PATH has already been reported. + * Therefore, we can call this recursively. + * + * Uses SCRATCH_BUFFER for temporary string contents. + */ +static svn_error_t * +do_list(svn_fs_root_t *root, + const char *path, + const apr_array_header_t *patterns, + svn_depth_t depth, + svn_boolean_t path_info_only, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_repos_dirent_receiver_t receiver, + void *receiver_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + svn_membuf_t *scratch_buffer, + apr_pool_t *scratch_pool) +{ + apr_hash_t *entries; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + apr_hash_index_t *hi; + apr_array_header_t *sorted; + int i; + + /* Fetch all directory entries, filter and sort them. + * + * Performance trade-off: + * Constructing a full path vs. faster sort due to authz filtering. + * We filter according to DEPTH and PATTERNS only because constructing + * the full path required for authz is somewhat expensive and we don't + * want to do this twice while authz will rarely filter paths out. + */ + SVN_ERR(svn_fs_dir_entries(&entries, root, path, scratch_pool)); + sorted = apr_array_make(scratch_pool, apr_hash_count(entries), + sizeof(filtered_dirent_t)); + for (hi = apr_hash_first(scratch_pool, entries); hi; hi = apr_hash_next(hi)) + { + filtered_dirent_t filtered; + svn_pool_clear(iterpool); + + filtered.dirent = apr_hash_this_val(hi); + + /* Skip directories if we want to report files only. */ + if (filtered.dirent->kind == svn_node_dir && depth == svn_depth_files) + continue; + + /* We can skip files that don't match any of the search patterns. */ + filtered.is_match = matches_any(filtered.dirent->name, patterns, + scratch_buffer); + if (!filtered.is_match && filtered.dirent->kind == svn_node_file) + continue; + + APR_ARRAY_PUSH(sorted, filtered_dirent_t) = filtered; + } + + svn_sort__array(sorted, compare_filtered_dirent); + + /* Iterate over all remaining directory entries and report them. + * Recurse into sub-directories if requested. */ + for (i = 0; i < sorted->nelts; ++i) + { + const char *sub_path; + filtered_dirent_t *filtered; + svn_fs_dirent_t *dirent; + + svn_pool_clear(iterpool); + + filtered = &APR_ARRAY_IDX(sorted, i, filtered_dirent_t); + dirent = filtered->dirent; + + /* Skip paths that we don't have access to? */ + sub_path = svn_dirent_join(path, dirent->name, iterpool); + if (authz_read_func) + { + svn_boolean_t has_access; + SVN_ERR(authz_read_func(&has_access, root, sub_path, + authz_read_baton, iterpool)); + if (!has_access) + continue; + } + + /* Report entry, if it passed the filter. */ + if (filtered->is_match) + SVN_ERR(report_dirent(root, sub_path, dirent->kind, path_info_only, + receiver, receiver_baton, iterpool)); + + /* Check for cancellation before recursing down. This should be + * slightly more responsive for deep trees. */ + if (cancel_func) + SVN_ERR(cancel_func(cancel_baton)); + + /* Recurse on directories. */ + if (depth == svn_depth_infinity && dirent->kind == svn_node_dir) + SVN_ERR(do_list(root, sub_path, patterns, svn_depth_infinity, + path_info_only, authz_read_func, authz_read_baton, + receiver, receiver_baton, cancel_func, + cancel_baton, scratch_buffer, iterpool)); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_repos_list(svn_fs_root_t *root, + const char *path, + const apr_array_header_t *patterns, + svn_depth_t depth, + svn_boolean_t path_info_only, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + svn_repos_dirent_receiver_t receiver, + void *receiver_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *scratch_pool) +{ + svn_membuf_t scratch_buffer; + + /* Parameter check. */ + svn_node_kind_t kind; + if (depth < svn_depth_empty) + return svn_error_createf(SVN_ERR_REPOS_BAD_ARGS, NULL, + "Invalid depth '%d' in svn_repos_list", depth); + + /* Do we have access this sub-tree? */ + if (authz_read_func) + { + svn_boolean_t has_access; + SVN_ERR(authz_read_func(&has_access, root, path, authz_read_baton, + scratch_pool)); + if (!has_access) + return SVN_NO_ERROR; + } + + /* Does the sub-tree even exist? + * + * Note that we must do this after the authz check to not indirectly + * confirm the existence of PATH. */ + SVN_ERR(svn_fs_check_path(&kind, root, path, scratch_pool)); + if (kind == svn_node_file) + { + /* There is no recursion on files. */ + depth = svn_depth_empty; + } + else if (kind != svn_node_dir) + { + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("Path '%s' not found"), path); + } + + /* Special case: Empty pattern list. + * We don't want the server to waste time here. */ + if (patterns && patterns->nelts == 0) + return SVN_NO_ERROR; + + /* We need a scratch buffer for temporary string data. + * Create one with a reasonable initial size. */ + svn_membuf__create(&scratch_buffer, 256, scratch_pool); + + /* Actually report PATH, if it passes the filters. */ + if (matches_any(svn_dirent_dirname(path, scratch_pool), patterns, + &scratch_buffer)) + SVN_ERR(report_dirent(root, path, kind, path_info_only, + receiver, receiver_baton, scratch_pool)); + + /* Report directory contents if requested. */ + if (depth > svn_depth_empty) + SVN_ERR(do_list(root, path, patterns, depth, + path_info_only, authz_read_func, authz_read_baton, + receiver, receiver_baton, cancel_func, cancel_baton, + &scratch_buffer, scratch_pool)); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_repos/load-fs-vtable.c b/subversion/libsvn_repos/load-fs-vtable.c index ca3a5cd34d17..f6c6bf660b12 100644 --- a/subversion/libsvn_repos/load-fs-vtable.c +++ b/subversion/libsvn_repos/load-fs-vtable.c @@ -55,6 +55,7 @@ struct parse_baton svn_boolean_t use_history; svn_boolean_t validate_props; svn_boolean_t ignore_dates; + svn_boolean_t normalize_props; svn_boolean_t use_pre_commit_hook; svn_boolean_t use_post_commit_hook; enum svn_repos_load_uuid uuid_action; @@ -163,8 +164,12 @@ change_rev_prop(svn_repos_t *repos, const char *name, const svn_string_t *value, svn_boolean_t validate_props, + svn_boolean_t normalize_props, apr_pool_t *pool) { + if (normalize_props) + SVN_ERR(svn_repos__normalize_prop(&value, NULL, name, value, pool, pool)); + if (validate_props) return svn_repos_fs_change_rev_prop4(repos, revision, NULL, name, NULL, value, FALSE, FALSE, @@ -624,14 +629,6 @@ maybe_add_with_history(struct node_baton *nb, } static svn_error_t * -magic_header_record(int version, - void *parse_baton, - apr_pool_t *pool) -{ - return SVN_NO_ERROR; -} - -static svn_error_t * uuid_record(const char *uuid, void *parse_baton, apr_pool_t *pool) @@ -1022,7 +1019,8 @@ close_revision(void *baton) apr_array_header_t *diff; int i; - SVN_ERR(svn_fs_revision_proplist(&orig_props, pb->fs, 0, rb->pool)); + SVN_ERR(svn_fs_revision_proplist2(&orig_props, pb->fs, 0, TRUE, + rb->pool, rb->pool)); new_props = svn_prop_array_to_hash(rb->revprops, rb->pool); SVN_ERR(svn_prop_diffs(&diff, new_props, orig_props, rb->pool)); @@ -1031,7 +1029,8 @@ close_revision(void *baton) const svn_prop_t *prop = &APR_ARRAY_IDX(diff, i, svn_prop_t); SVN_ERR(change_rev_prop(pb->repos, 0, prop->name, prop->value, - pb->validate_props, rb->pool)); + pb->validate_props, pb->normalize_props, + rb->pool)); } } @@ -1049,6 +1048,23 @@ close_revision(void *baton) prop->value = NULL; } + if (rb->pb->normalize_props) + { + apr_pool_t *iterpool; + int i; + + iterpool = svn_pool_create(rb->pool); + for (i = 0; i < rb->revprops->nelts; i++) + { + svn_prop_t *prop = &APR_ARRAY_IDX(rb->revprops, i, svn_prop_t); + + svn_pool_clear(iterpool); + SVN_ERR(svn_repos__normalize_prop(&prop->value, NULL, prop->name, + prop->value, rb->pool, iterpool)); + } + svn_pool_destroy(iterpool); + } + /* Apply revision property changes. */ if (rb->pb->validate_props) SVN_ERR(svn_repos_fs_change_txn_props(rb->txn, rb->revprops, rb->pool)); @@ -1165,7 +1181,7 @@ close_revision(void *baton) svn_error_t * -svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **callbacks, +svn_repos_get_fs_build_parser6(const svn_repos_parse_fns3_t **callbacks, void **parse_baton, svn_repos_t *repos, svn_revnum_t start_rev, @@ -1177,6 +1193,7 @@ svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **callbacks, svn_boolean_t use_pre_commit_hook, svn_boolean_t use_post_commit_hook, svn_boolean_t ignore_dates, + svn_boolean_t normalize_props, svn_repos_notify_func_t notify_func, void *notify_baton, apr_pool_t *pool) @@ -1194,7 +1211,7 @@ svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **callbacks, if (SVN_IS_VALID_REVNUM(start_rev)) SVN_ERR_ASSERT(start_rev <= end_rev); - parser->magic_header_record = magic_header_record; + parser->magic_header_record = NULL; parser->uuid_record = uuid_record; parser->new_revision_record = new_revision_record; parser->new_node_record = new_node_record; @@ -1225,6 +1242,7 @@ svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **callbacks, pb->use_pre_commit_hook = use_pre_commit_hook; pb->use_post_commit_hook = use_post_commit_hook; pb->ignore_dates = ignore_dates; + pb->normalize_props = normalize_props; *callbacks = parser; *parse_baton = pb; @@ -1233,7 +1251,7 @@ svn_repos_get_fs_build_parser5(const svn_repos_parse_fns3_t **callbacks, svn_error_t * -svn_repos_load_fs5(svn_repos_t *repos, +svn_repos_load_fs6(svn_repos_t *repos, svn_stream_t *dumpstream, svn_revnum_t start_rev, svn_revnum_t end_rev, @@ -1243,6 +1261,7 @@ svn_repos_load_fs5(svn_repos_t *repos, svn_boolean_t use_post_commit_hook, svn_boolean_t validate_props, svn_boolean_t ignore_dates, + svn_boolean_t normalize_props, svn_repos_notify_func_t notify_func, void *notify_baton, svn_cancel_func_t cancel_func, @@ -1254,7 +1273,7 @@ svn_repos_load_fs5(svn_repos_t *repos, /* This is really simple. */ - SVN_ERR(svn_repos_get_fs_build_parser5(&parser, &parse_baton, + SVN_ERR(svn_repos_get_fs_build_parser6(&parser, &parse_baton, repos, start_rev, end_rev, TRUE, /* look for copyfrom revs */ @@ -1264,6 +1283,7 @@ svn_repos_load_fs5(svn_repos_t *repos, use_pre_commit_hook, use_post_commit_hook, ignore_dates, + normalize_props, notify_func, notify_baton, pool)); @@ -1271,3 +1291,226 @@ svn_repos_load_fs5(svn_repos_t *repos, return svn_repos_parse_dumpstream3(dumpstream, parser, parse_baton, FALSE, cancel_func, cancel_baton, pool); } + +/*----------------------------------------------------------------------*/ + +/** The same functionality for revprops only **/ + +/* Implement svn_repos_parse_fns3_t.new_revision_record. + * + * Because the revision is supposed to already exist, we don't need to + * start transactions etc. */ +static svn_error_t * +revprops_new_revision_record(void **revision_baton, + apr_hash_t *headers, + void *parse_baton, + apr_pool_t *pool) +{ + struct parse_baton *pb = parse_baton; + struct revision_baton *rb; + + rb = make_revision_baton(headers, pb, pool); + + /* If we're skipping this revision, try to notify someone. */ + if (rb->skipped && pb->notify_func) + { + /* ### TODO: Use proper scratch pool instead of pb->notify_pool */ + svn_repos_notify_t *notify = svn_repos_notify_create( + svn_repos_notify_load_skipped_rev, + pb->notify_pool); + + notify->old_revision = rb->rev; + pb->notify_func(pb->notify_baton, notify, pb->notify_pool); + svn_pool_clear(pb->notify_pool); + } + + /* If we're parsing revision 0, only the revision props are (possibly) + interesting to us: when loading the stream into an empty + filesystem, then we want new filesystem's revision 0 to have the + same props. Otherwise, we just ignore revision 0 in the stream. */ + + *revision_baton = rb; + return SVN_NO_ERROR; +} + +/* Implement svn_repos_parse_fns3_t.close_revision. + * + * Simply set the revprops we previously parsed and send notifications. + * This is the place where we will detect missing revisions. */ +static svn_error_t * +revprops_close_revision(void *baton) +{ + struct revision_baton *rb = baton; + struct parse_baton *pb = rb->pb; + apr_hash_t *orig_props; + apr_hash_t *new_props; + apr_array_header_t *diff; + int i; + + /* If we're skipping this revision we're done here. */ + if (rb->skipped) + return SVN_NO_ERROR; + + /* If the dumpstream doesn't have an 'svn:date' property and we + aren't ignoring the dates in the dumpstream altogether, remove + any 'svn:date' revision property that was set by FS layer when + the TXN was created. */ + if (! (pb->ignore_dates || rb->datestamp)) + { + svn_prop_t *prop = &APR_ARRAY_PUSH(rb->revprops, svn_prop_t); + prop->name = SVN_PROP_REVISION_DATE; + prop->value = NULL; + } + + SVN_ERR(svn_fs_revision_proplist2(&orig_props, pb->fs, rb->rev, TRUE, + rb->pool, rb->pool)); + new_props = svn_prop_array_to_hash(rb->revprops, rb->pool); + SVN_ERR(svn_prop_diffs(&diff, new_props, orig_props, rb->pool)); + + for (i = 0; i < diff->nelts; i++) + { + const svn_prop_t *prop = &APR_ARRAY_IDX(diff, i, svn_prop_t); + + SVN_ERR(change_rev_prop(pb->repos, rb->rev, prop->name, prop->value, + pb->validate_props, pb->normalize_props, + rb->pool)); + } + + if (pb->notify_func) + { + /* ### TODO: Use proper scratch pool instead of pb->notify_pool */ + svn_repos_notify_t *notify = svn_repos_notify_create( + svn_repos_notify_load_revprop_set, + pb->notify_pool); + + notify->new_revision = rb->rev; + notify->old_revision = SVN_INVALID_REVNUM; + pb->notify_func(pb->notify_baton, notify, pb->notify_pool); + svn_pool_clear(pb->notify_pool); + } + + return SVN_NO_ERROR; +} + +/* Set *CALLBACKS and *PARSE_BATON to a vtable parser which commits new + * revisions to the fs in REPOS. Allocate the objects in RESULT_POOL. + * + * START_REV and END_REV act as filters, the lower and upper (inclusive) + * range values of revisions in DUMPSTREAM which will be loaded. Either + * both of these values are #SVN_INVALID_REVNUM (in which case no + * revision-based filtering occurs at all), or both are valid revisions + * (where START_REV is older than or equivalent to END_REV). + * + * START_REV and END_REV act as filters, the lower and upper (inclusive) + * range values of revisions which will + * be loaded. Either both of these values are #SVN_INVALID_REVNUM (in + * which case no revision-based filtering occurs at all), or both are + * valid revisions (where START_REV is older than or equivalent to + * END_REV). They refer to dump stream revision numbers rather than + * committed revision numbers. + * + * If VALIDATE_PROPS is set, then validate Subversion revision properties + * (those in the svn: namespace) against established rules for those things. + * + * If IGNORE_DATES is set, ignore any revision datestamps found in + * DUMPSTREAM, keeping whatever timestamps the revisions currently have. + * + * If NORMALIZE_PROPS is set, attempt to normalize invalid Subversion + * revision and node properties (those in the svn: namespace) so that + * their values would follow the established rules for them. Currently, + * this means translating non-LF line endings in the property values to LF. + */ +static svn_error_t * +build_revprop_parser(const svn_repos_parse_fns3_t **callbacks, + void **parse_baton, + svn_repos_t *repos, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t validate_props, + svn_boolean_t ignore_dates, + svn_boolean_t normalize_props, + svn_repos_notify_func_t notify_func, + void *notify_baton, + apr_pool_t *result_pool) +{ + svn_repos_parse_fns3_t *parser = apr_pcalloc(result_pool, sizeof(*parser)); + struct parse_baton *pb = apr_pcalloc(result_pool, sizeof(*pb)); + + SVN_ERR_ASSERT((SVN_IS_VALID_REVNUM(start_rev) && + SVN_IS_VALID_REVNUM(end_rev)) + || ((! SVN_IS_VALID_REVNUM(start_rev)) && + (! SVN_IS_VALID_REVNUM(end_rev)))); + if (SVN_IS_VALID_REVNUM(start_rev)) + SVN_ERR_ASSERT(start_rev <= end_rev); + + parser->magic_header_record = NULL; + parser->uuid_record = uuid_record; + parser->new_revision_record = revprops_new_revision_record; + parser->new_node_record = NULL; + parser->set_revision_property = set_revision_property; + parser->set_node_property = NULL; + parser->remove_node_props = NULL; + parser->set_fulltext = NULL; + parser->close_node = NULL; + parser->close_revision = revprops_close_revision; + parser->delete_node_property = NULL; + parser->apply_textdelta = NULL; + + pb->repos = repos; + pb->fs = svn_repos_fs(repos); + pb->use_history = FALSE; + pb->validate_props = validate_props; + pb->notify_func = notify_func; + pb->notify_baton = notify_baton; + pb->uuid_action = svn_repos_load_uuid_ignore; /* Never touch the UUID. */ + pb->parent_dir = NULL; + pb->pool = result_pool; + pb->notify_pool = svn_pool_create(result_pool); + pb->rev_map = NULL; + pb->oldest_dumpstream_rev = SVN_INVALID_REVNUM; + pb->last_rev_mapped = SVN_INVALID_REVNUM; + pb->start_rev = start_rev; + pb->end_rev = end_rev; + pb->use_pre_commit_hook = FALSE; + pb->use_post_commit_hook = FALSE; + pb->ignore_dates = ignore_dates; + pb->normalize_props = normalize_props; + + *callbacks = parser; + *parse_baton = pb; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_repos_load_fs_revprops(svn_repos_t *repos, + svn_stream_t *dumpstream, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_boolean_t validate_props, + svn_boolean_t ignore_dates, + svn_boolean_t normalize_props, + svn_repos_notify_func_t notify_func, + void *notify_baton, + svn_cancel_func_t cancel_func, + void *cancel_baton, + apr_pool_t *scratch_pool) +{ + const svn_repos_parse_fns3_t *parser; + void *parse_baton; + + /* This is really simple. */ + + SVN_ERR(build_revprop_parser(&parser, &parse_baton, + repos, + start_rev, end_rev, + validate_props, + ignore_dates, + normalize_props, + notify_func, + notify_baton, + scratch_pool)); + + return svn_repos_parse_dumpstream3(dumpstream, parser, parse_baton, FALSE, + cancel_func, cancel_baton, scratch_pool); +} diff --git a/subversion/libsvn_repos/load.c b/subversion/libsvn_repos/load.c index 96eda85cb183..27cf4a174552 100644 --- a/subversion/libsvn_repos/load.c +++ b/subversion/libsvn_repos/load.c @@ -385,7 +385,135 @@ parse_format_version(int *version, return SVN_NO_ERROR; } +/*----------------------------------------------------------------------*/ + +/** Dummy callback implementations for functions not provided by the user **/ +static svn_error_t * +dummy_handler_magic_header_record(int version, + void *parse_baton, + apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_uuid_record(const char *uuid, + void *parse_baton, + apr_pool_t *pool) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_new_revision_record(void **revision_baton, + apr_hash_t *headers, + void *parse_baton, + apr_pool_t *pool) +{ + *revision_baton = NULL; + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_new_node_record(void **node_baton, + apr_hash_t *headers, + void *revision_baton, + apr_pool_t *pool) +{ + *node_baton = NULL; + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_set_revision_property(void *revision_baton, + const char *name, + const svn_string_t *value) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_set_node_property(void *node_baton, + const char *name, + const svn_string_t *value) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_delete_node_property(void *node_baton, + const char *name) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_remove_node_props(void *node_baton) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_set_fulltext(svn_stream_t **stream, + void *node_baton) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_apply_textdelta(svn_txdelta_window_handler_t *handler, + void **handler_baton, + void *node_baton) +{ + /* Only called by parse_text_block() and that tests for NULL handlers. */ + *handler = NULL; + *handler_baton = NULL; + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_close_node(void *node_baton) +{ + return SVN_NO_ERROR; +} + +static svn_error_t * +dummy_handler_close_revision(void *revision_baton) +{ + return SVN_NO_ERROR; +} + +/* Helper macro to copy the function pointer SOURCE->NAME to DEST->NAME. + * If the source pointer is NULL, pick the corresponding dummy handler + * instead. */ +#define SET_VTABLE_ENTRY(dest, source, name) \ + dest->name = provided->name ? provided->name : dummy_handler_##name + +/* Return a copy of PROVIDED with all NULL callbacks replaced by a dummy + * handler. Allocate the result in RESULT_POOL. */ +static const svn_repos_parse_fns3_t * +complete_vtable(const svn_repos_parse_fns3_t *provided, + apr_pool_t *result_pool) +{ + svn_repos_parse_fns3_t *completed = apr_pcalloc(result_pool, + sizeof(*completed)); + + SET_VTABLE_ENTRY(completed, provided, magic_header_record); + SET_VTABLE_ENTRY(completed, provided, uuid_record); + SET_VTABLE_ENTRY(completed, provided, new_revision_record); + SET_VTABLE_ENTRY(completed, provided, new_node_record); + SET_VTABLE_ENTRY(completed, provided, set_revision_property); + SET_VTABLE_ENTRY(completed, provided, set_node_property); + SET_VTABLE_ENTRY(completed, provided, delete_node_property); + SET_VTABLE_ENTRY(completed, provided, remove_node_props); + SET_VTABLE_ENTRY(completed, provided, set_fulltext); + SET_VTABLE_ENTRY(completed, provided, apply_textdelta); + SET_VTABLE_ENTRY(completed, provided, close_node); + SET_VTABLE_ENTRY(completed, provided, close_revision); + + return completed; +} /*----------------------------------------------------------------------*/ @@ -410,6 +538,10 @@ svn_repos_parse_dumpstream3(svn_stream_t *stream, apr_pool_t *nodepool = svn_pool_create(pool); int version; + /* Make sure we can blindly invoke callbacks. */ + parse_fns = complete_vtable(parse_fns, pool); + + /* Start parsing process. */ SVN_ERR(svn_stream_readline(stream, &linebuf, "\n", &eof, linepool)); if (eof) return stream_ran_dry(); diff --git a/subversion/libsvn_repos/log.c b/subversion/libsvn_repos/log.c index 82caf0219ea8..7dec5dd8d5b7 100644 --- a/subversion/libsvn_repos/log.c +++ b/subversion/libsvn_repos/log.c @@ -43,8 +43,72 @@ #include "private/svn_mergeinfo_private.h" #include "private/svn_subr_private.h" #include "private/svn_sorts_private.h" +#include "private/svn_string_private.h" +/* This is a mere convenience struct such that we don't need to pass that + many parameters around individually. */ +typedef struct log_callbacks_t +{ + svn_repos_path_change_receiver_t path_change_receiver; + void *path_change_receiver_baton; + svn_repos_log_entry_receiver_t revision_receiver; + void *revision_receiver_baton; + svn_repos_authz_func_t authz_read_func; + void *authz_read_baton; +} log_callbacks_t; + + +svn_repos_path_change_t * +svn_repos_path_change_create(apr_pool_t *result_pool) +{ + svn_repos_path_change_t *change = apr_pcalloc(result_pool, sizeof(*change)); + + change->path.data = ""; + change->change_kind = svn_fs_path_change_reset; + change->mergeinfo_mod = svn_tristate_unknown; + change->copyfrom_rev = SVN_INVALID_REVNUM; + + return change; +} + +svn_repos_path_change_t * +svn_repos_path_change_dup(svn_repos_path_change_t *change, + apr_pool_t *result_pool) +{ + svn_repos_path_change_t *new_change = apr_pmemdup(result_pool, change, + sizeof(*new_change)); + + new_change->path.data = apr_pstrmemdup(result_pool, change->path.data, + change->path.len); + if (change->copyfrom_path) + new_change->copyfrom_path = apr_pstrdup(result_pool, + change->copyfrom_path); + + return new_change; +} + +svn_repos_log_entry_t * +svn_repos_log_entry_create(apr_pool_t *result_pool) +{ + svn_repos_log_entry_t *log_entry = apr_pcalloc(result_pool, + sizeof(*log_entry)); + + return log_entry; +} + +svn_repos_log_entry_t * +svn_repos_log_entry_dup(const svn_repos_log_entry_t *log_entry, + apr_pool_t *result_pool) +{ + svn_repos_log_entry_t *new_entry = apr_pmemdup(result_pool, log_entry, + sizeof(*new_entry)); + + if (log_entry->revprops) + new_entry->revprops = svn_prop_hash_dup(log_entry->revprops, result_pool); + + return new_entry; +} svn_error_t * svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, @@ -56,11 +120,11 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, { svn_fs_t *fs = svn_repos_fs(repos); svn_fs_root_t *rev_root; - apr_hash_t *changes; - apr_hash_index_t *hi; + svn_fs_path_change_iterator_t *iterator; + svn_fs_path_change3_t *change; svn_boolean_t found_readable = FALSE; svn_boolean_t found_unreadable = FALSE; - apr_pool_t *subpool; + apr_pool_t *iterpool; /* By default, we'll grant full read access to REVISION. */ *access_level = svn_repos_revision_access_full; @@ -71,25 +135,27 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, /* Fetch the changes associated with REVISION. */ SVN_ERR(svn_fs_revision_root(&rev_root, fs, revision, pool)); - SVN_ERR(svn_fs_paths_changed2(&changes, rev_root, pool)); + SVN_ERR(svn_fs_paths_changed3(&iterator, rev_root, pool, pool)); + SVN_ERR(svn_fs_path_change_get(&change, iterator)); - /* No changed paths? We're done. */ - if (apr_hash_count(changes) == 0) + /* No changed paths? We're done. + + Note that the check at "decision:" assumes that at least one + path has been processed. So, this actually affects functionality. */ + if (!change) return SVN_NO_ERROR; /* Otherwise, we have to check the readability of each changed path, or at least enough to answer the question asked. */ - subpool = svn_pool_create(pool); - for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) + iterpool = svn_pool_create(pool); + while (change) { - const char *key = apr_hash_this_key(hi); - svn_fs_path_change2_t *change = apr_hash_this_val(hi); svn_boolean_t readable; - svn_pool_clear(subpool); + svn_pool_clear(iterpool); - SVN_ERR(authz_read_func(&readable, rev_root, key, - authz_read_baton, subpool)); + SVN_ERR(authz_read_func(&readable, rev_root, change->path.data, + authz_read_baton, iterpool)); if (! readable) found_unreadable = TRUE; else @@ -109,15 +175,16 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, svn_revnum_t copyfrom_rev; SVN_ERR(svn_fs_copied_from(©from_rev, ©from_path, - rev_root, key, subpool)); + rev_root, change->path.data, + iterpool)); if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) { svn_fs_root_t *copyfrom_root; SVN_ERR(svn_fs_revision_root(©from_root, fs, - copyfrom_rev, subpool)); + copyfrom_rev, iterpool)); SVN_ERR(authz_read_func(&readable, copyfrom_root, copyfrom_path, - authz_read_baton, subpool)); + authz_read_baton, iterpool)); if (! readable) found_unreadable = TRUE; @@ -134,10 +201,12 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, default: break; } + + SVN_ERR(svn_fs_path_change_get(&change, iterator)); } decision: - svn_pool_destroy(subpool); + svn_pool_destroy(iterpool); /* Either every changed path was unreadable... */ if (! found_readable) @@ -152,21 +221,14 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, } -/* Store as keys in CHANGED the paths of all node in ROOT that show a - * significant change. "Significant" means that the text or - * properties of the node were changed, or that the node was added or - * deleted. - * - * The CHANGED hash set and its keys and values are allocated in POOL; - * keys are const char * paths and values are svn_log_changed_path_t. +/* Find all significant changes under ROOT and, if not NULL, report them + * to the CALLBACKS->PATH_CHANGE_RECEIVER. "Significant" means that the + * text or properties of the node were changed, or that the node was added + * or deleted. * - * To prevent changes from being processed over and over again, the - * changed paths for ROOT may be passed in PREFETCHED_CHANGES. If the - * latter is NULL, we will request the list inside this function. - * - * If optional AUTHZ_READ_FUNC is non-NULL, then use it (with - * AUTHZ_READ_BATON and FS) to check whether each changed-path (and - * copyfrom_path) is readable: + * If optional CALLBACKS->AUTHZ_READ_FUNC is non-NULL, then use it (with + * CALLBACKS->AUTHZ_READ_BATON and FS) to check whether each changed-path + * (and copyfrom_path) is readable: * * - If absolutely every changed-path (and copyfrom_path) is * readable, then return the full CHANGED hash, and set @@ -184,40 +246,22 @@ svn_repos_check_revision_access(svn_repos_revision_access_level_t *access_level, */ static svn_error_t * detect_changed(svn_repos_revision_access_level_t *access_level, - apr_hash_t **changed, svn_fs_root_t *root, svn_fs_t *fs, - apr_hash_t *prefetched_changes, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, - apr_pool_t *pool) + const log_callbacks_t *callbacks, + apr_pool_t *scratch_pool) { - apr_hash_t *changes = prefetched_changes; - apr_hash_index_t *hi; + svn_fs_path_change_iterator_t *iterator; + svn_fs_path_change3_t *change; apr_pool_t *iterpool; svn_boolean_t found_readable = FALSE; svn_boolean_t found_unreadable = FALSE; - /* If we create the CHANGES hash ourselves, we can reuse it as the - * result hash as it contains the exact same keys - but with _all_ - * values being replaced by structs of a different type. */ - if (changes == NULL) - { - SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); - - /* If we are going to filter the results, we won't use the exact - * same keys but put them into a new hash. */ - if (authz_read_func) - *changed = svn_hash__make(pool); - else - *changed = changes; - } - else - { - *changed = svn_hash__make(pool); - } + /* Retrieve the first change in the list. */ + SVN_ERR(svn_fs_paths_changed3(&iterator, root, scratch_pool, scratch_pool)); + SVN_ERR(svn_fs_path_change_get(&change, iterator)); - if (apr_hash_count(changes) == 0) + if (!change) { /* No paths changed in this revision? Uh, sure, I guess the revision is readable, then. */ @@ -225,30 +269,26 @@ detect_changed(svn_repos_revision_access_level_t *access_level, return SVN_NO_ERROR; } - iterpool = svn_pool_create(pool); - for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) + iterpool = svn_pool_create(scratch_pool); + while (change) { /* NOTE: Much of this loop is going to look quite similar to svn_repos_check_revision_access(), but we have to do more things here, so we'll live with the duplication. */ - const char *path = apr_hash_this_key(hi); - apr_ssize_t path_len = apr_hash_this_key_len(hi); - svn_fs_path_change2_t *change = apr_hash_this_val(hi); - char action; - svn_log_changed_path2_t *item; - + const char *path = change->path.data; svn_pool_clear(iterpool); /* Skip path if unreadable. */ - if (authz_read_func) + if (callbacks->authz_read_func) { svn_boolean_t readable; - SVN_ERR(authz_read_func(&readable, - root, path, - authz_read_baton, iterpool)); + SVN_ERR(callbacks->authz_read_func(&readable, root, path, + callbacks->authz_read_baton, + iterpool)); if (! readable) { found_unreadable = TRUE; + SVN_ERR(svn_fs_path_change_get(&change, iterator)); continue; } } @@ -256,41 +296,9 @@ detect_changed(svn_repos_revision_access_level_t *access_level, /* At least one changed-path was readable. */ found_readable = TRUE; - switch (change->change_kind) - { - case svn_fs_path_change_reset: - continue; - - case svn_fs_path_change_add: - action = 'A'; - break; - - case svn_fs_path_change_replace: - action = 'R'; - break; - - case svn_fs_path_change_delete: - action = 'D'; - break; - - case svn_fs_path_change_modify: - default: - action = 'M'; - break; - } - - item = svn_log_changed_path2_create(pool); - item->action = action; - item->node_kind = change->node_kind; - item->copyfrom_rev = SVN_INVALID_REVNUM; - item->text_modified = change->text_mod ? svn_tristate_true - : svn_tristate_false; - item->props_modified = change->prop_mod ? svn_tristate_true - : svn_tristate_false; - /* Pre-1.6 revision files don't store the change path kind, so fetch it manually. */ - if (item->node_kind == svn_node_unknown) + if (change->node_kind == svn_node_unknown) { svn_fs_root_t *check_root = root; const char *check_path = path; @@ -315,18 +323,19 @@ detect_changed(svn_repos_revision_access_level_t *access_level, SVN_ERR(svn_fs_history_prev2(&history, history, TRUE, iterpool, iterpool)); - SVN_ERR(svn_fs_history_location(&parent_path, &prev_rev, history, - iterpool)); - SVN_ERR(svn_fs_revision_root(&check_root, fs, prev_rev, iterpool)); + SVN_ERR(svn_fs_history_location(&parent_path, &prev_rev, + history, iterpool)); + SVN_ERR(svn_fs_revision_root(&check_root, fs, prev_rev, + iterpool)); check_path = svn_fspath__join(parent_path, name, iterpool); } - SVN_ERR(svn_fs_check_path(&item->node_kind, check_root, check_path, + SVN_ERR(svn_fs_check_path(&change->node_kind, check_root, check_path, iterpool)); } - - if ((action == 'A') || (action == 'R')) + if ( (change->change_kind == svn_fs_path_change_add) + || (change->change_kind == svn_fs_path_change_replace)) { const char *copyfrom_path = change->copyfrom_path; svn_revnum_t copyfrom_rev = change->copyfrom_rev; @@ -338,35 +347,44 @@ detect_changed(svn_repos_revision_access_level_t *access_level, { SVN_ERR(svn_fs_copied_from(©from_rev, ©from_path, root, path, iterpool)); - copyfrom_path = apr_pstrdup(pool, copyfrom_path); + change->copyfrom_known = TRUE; } if (copyfrom_path && SVN_IS_VALID_REVNUM(copyfrom_rev)) { svn_boolean_t readable = TRUE; - if (authz_read_func) + if (callbacks->authz_read_func) { svn_fs_root_t *copyfrom_root; SVN_ERR(svn_fs_revision_root(©from_root, fs, copyfrom_rev, iterpool)); - SVN_ERR(authz_read_func(&readable, - copyfrom_root, copyfrom_path, - authz_read_baton, iterpool)); + SVN_ERR(callbacks->authz_read_func(&readable, + copyfrom_root, + copyfrom_path, + callbacks->authz_read_baton, + iterpool)); if (! readable) found_unreadable = TRUE; } if (readable) { - item->copyfrom_path = copyfrom_path; - item->copyfrom_rev = copyfrom_rev; + change->copyfrom_path = copyfrom_path; + change->copyfrom_rev = copyfrom_rev; } } } - apr_hash_set(*changed, path, path_len, item); + if (callbacks->path_change_receiver) + SVN_ERR(callbacks->path_change_receiver( + callbacks->path_change_receiver_baton, + change, + iterpool)); + + /* Next changed path. */ + SVN_ERR(svn_fs_path_change_get(&change, iterator)); } svn_pool_destroy(iterpool); @@ -593,23 +611,21 @@ next_history_rev(const apr_array_header_t *histories) /* Set *DELETED_MERGEINFO_CATALOG and *ADDED_MERGEINFO_CATALOG to catalogs describing how mergeinfo values on paths (which are the - keys of those catalogs) were changed in REV. If *PREFETCHED_CHANGES - already contains the changed paths for REV, use that. Otherwise, - request that data and return it in *PREFETCHED_CHANGES. */ + keys of those catalogs) were changed in REV. */ /* ### TODO: This would make a *great*, useful public function, ### svn_repos_fs_mergeinfo_changed()! -- cmpilato */ static svn_error_t * fs_mergeinfo_changed(svn_mergeinfo_catalog_t *deleted_mergeinfo_catalog, svn_mergeinfo_catalog_t *added_mergeinfo_catalog, - apr_hash_t **prefetched_changes, svn_fs_t *fs, svn_revnum_t rev, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { svn_fs_root_t *root; - apr_pool_t *iterpool; - apr_hash_index_t *hi; + apr_pool_t *iterpool, *iterator_pool; + svn_fs_path_change_iterator_t *iterator; + svn_fs_path_change3_t *change; svn_boolean_t any_mergeinfo = FALSE; svn_boolean_t any_copy = FALSE; @@ -621,56 +637,69 @@ fs_mergeinfo_changed(svn_mergeinfo_catalog_t *deleted_mergeinfo_catalog, if (rev == 0) return SVN_NO_ERROR; + /* FS iterators are potentially heavy objects. + * Hold them in a separate pool to clean them up asap. */ + iterator_pool = svn_pool_create(scratch_pool); + /* We're going to use the changed-paths information for REV to narrow down our search. */ SVN_ERR(svn_fs_revision_root(&root, fs, rev, scratch_pool)); - if (*prefetched_changes == NULL) - SVN_ERR(svn_fs_paths_changed2(prefetched_changes, root, scratch_pool)); + SVN_ERR(svn_fs_paths_changed3(&iterator, root, iterator_pool, + iterator_pool)); + SVN_ERR(svn_fs_path_change_get(&change, iterator)); /* Look for copies and (potential) mergeinfo changes. - We will use both flags to take shortcuts further down the road. */ - for (hi = apr_hash_first(scratch_pool, *prefetched_changes); - hi; - hi = apr_hash_next(hi)) + We will use both flags to take shortcuts further down the road. + + The critical information here is whether there are any copies + because that greatly influences the costs for log processing. + So, it is faster to iterate over the changes twice - in the worst + case b/c most times there is no m/i at all and we exit out early + without any overhead. + */ + while (change && (!any_mergeinfo || !any_copy)) { - svn_fs_path_change2_t *change = apr_hash_this_val(hi); - /* If there was a prop change and we are not positive that _no_ mergeinfo change happened, we must assume that it might have. */ if (change->mergeinfo_mod != svn_tristate_false && change->prop_mod) any_mergeinfo = TRUE; - switch (change->change_kind) - { - case svn_fs_path_change_add: - case svn_fs_path_change_replace: - any_copy = TRUE; - break; + if ( (change->change_kind == svn_fs_path_change_add) + || (change->change_kind == svn_fs_path_change_replace)) + any_copy = TRUE; - default: - break; - } + SVN_ERR(svn_fs_path_change_get(&change, iterator)); } /* No potential mergeinfo changes? We're done. */ if (! any_mergeinfo) - return SVN_NO_ERROR; + { + svn_pool_destroy(iterator_pool); + return SVN_NO_ERROR; + } + + /* There is or may be some m/i change. Look closely now. */ + svn_pool_clear(iterator_pool); + SVN_ERR(svn_fs_paths_changed3(&iterator, root, iterator_pool, + iterator_pool)); /* Loop over changes, looking for anything that might carry an svn:mergeinfo change and is one of our paths of interest, or a child or [grand]parent directory thereof. */ iterpool = svn_pool_create(scratch_pool); - for (hi = apr_hash_first(scratch_pool, *prefetched_changes); - hi; - hi = apr_hash_next(hi)) + while (TRUE) { const char *changed_path; - svn_fs_path_change2_t *change = apr_hash_this_val(hi); const char *base_path = NULL; svn_revnum_t base_rev = SVN_INVALID_REVNUM; svn_fs_root_t *base_root = NULL; svn_string_t *prev_mergeinfo_value = NULL, *mergeinfo_value; + /* Next change. */ + SVN_ERR(svn_fs_path_change_get(&change, iterator)); + if (!change) + break; + /* Cheap pre-checks that don't require memory allocation etc. */ /* No mergeinfo change? -> nothing to do here. */ @@ -682,7 +711,7 @@ fs_mergeinfo_changed(svn_mergeinfo_catalog_t *deleted_mergeinfo_catalog, continue; /* Begin actual processing */ - changed_path = apr_hash_this_key(hi); + changed_path = change->path.data; svn_pool_clear(iterpool); switch (change->change_kind) @@ -840,20 +869,18 @@ fs_mergeinfo_changed(svn_mergeinfo_catalog_t *deleted_mergeinfo_catalog, } svn_pool_destroy(iterpool); + svn_pool_destroy(iterator_pool); + return SVN_NO_ERROR; } /* Determine what (if any) mergeinfo for PATHS was modified in revision REV, returning the differences for added mergeinfo in - *ADDED_MERGEINFO and deleted mergeinfo in *DELETED_MERGEINFO. - If *PREFETCHED_CHANGES already contains the changed paths for - REV, use that. Otherwise, request that data and return it in - *PREFETCHED_CHANGES. */ + *ADDED_MERGEINFO and deleted mergeinfo in *DELETED_MERGEINFO. */ static svn_error_t * get_combined_mergeinfo_changes(svn_mergeinfo_t *added_mergeinfo, svn_mergeinfo_t *deleted_mergeinfo, - apr_hash_t **prefetched_changes, svn_fs_t *fs, const apr_array_header_t *paths, svn_revnum_t rev, @@ -882,7 +909,6 @@ get_combined_mergeinfo_changes(svn_mergeinfo_t *added_mergeinfo, /* Fetch the mergeinfo changes for REV. */ err = fs_mergeinfo_changed(&deleted_mergeinfo_catalog, &added_mergeinfo_catalog, - prefetched_changes, fs, rev, scratch_pool, scratch_pool); if (err) @@ -1071,38 +1097,31 @@ get_combined_mergeinfo_changes(svn_mergeinfo_t *added_mergeinfo, /* Fill LOG_ENTRY with history information in FS at REV. */ static svn_error_t * -fill_log_entry(svn_log_entry_t *log_entry, +fill_log_entry(svn_repos_log_entry_t *log_entry, svn_revnum_t rev, svn_fs_t *fs, - apr_hash_t *prefetched_changes, - svn_boolean_t discover_changed_paths, const apr_array_header_t *revprops, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, + const log_callbacks_t *callbacks, apr_pool_t *pool) { - apr_hash_t *r_props, *changed_paths = NULL; + apr_hash_t *r_props; svn_boolean_t get_revprops = TRUE, censor_revprops = FALSE; svn_boolean_t want_revprops = !revprops || revprops->nelts; /* Discover changed paths if the user requested them or if we need to check that they are readable. */ if ((rev > 0) - && (authz_read_func || discover_changed_paths)) + && (callbacks->authz_read_func || callbacks->path_change_receiver)) { svn_fs_root_t *newroot; svn_repos_revision_access_level_t access_level; SVN_ERR(svn_fs_revision_root(&newroot, fs, rev, pool)); - SVN_ERR(detect_changed(&access_level, &changed_paths, - newroot, fs, prefetched_changes, - authz_read_func, authz_read_baton, - pool)); + SVN_ERR(detect_changed(&access_level, newroot, fs, callbacks, pool)); if (access_level == svn_repos_revision_access_none) { /* All changed-paths are unreadable, so clear all fields. */ - changed_paths = NULL; get_revprops = FALSE; } else if (access_level == svn_repos_revision_access_partial) @@ -1112,17 +1131,13 @@ fill_log_entry(svn_log_entry_t *log_entry, missing from the hash.) */ censor_revprops = TRUE; } - - /* It may be the case that an authz func was passed in, but - the user still doesn't want to see any changed-paths. */ - if (! discover_changed_paths) - changed_paths = NULL; } if (get_revprops && want_revprops) { /* User is allowed to see at least some revprops. */ - SVN_ERR(svn_fs_revision_proplist(&r_props, fs, rev, pool)); + SVN_ERR(svn_fs_revision_proplist2(&r_props, fs, rev, FALSE, pool, + pool)); if (revprops == NULL) { /* Requested all revprops... */ @@ -1150,9 +1165,9 @@ fill_log_entry(svn_log_entry_t *log_entry, we want static initialization here and must therefore emulate strlen(x) by sizeof(x)-1. */ static const svn_string_t svn_prop_revision_author - = {SVN_PROP_REVISION_AUTHOR, sizeof(SVN_PROP_REVISION_AUTHOR)-1}; + = SVN__STATIC_STRING(SVN_PROP_REVISION_AUTHOR); static const svn_string_t svn_prop_revision_date - = {SVN_PROP_REVISION_DATE, sizeof(SVN_PROP_REVISION_DATE)-1}; + = SVN__STATIC_STRING(SVN_PROP_REVISION_DATE); /* often only the standard revprops got requested and delivered. In that case, we can simply pass the hash on. */ @@ -1192,20 +1207,83 @@ fill_log_entry(svn_log_entry_t *log_entry, } } - log_entry->changed_paths = changed_paths; - log_entry->changed_paths2 = changed_paths; log_entry->revision = rev; return SVN_NO_ERROR; } -/* Send a log message for REV to RECEIVER with its RECEIVER_BATON. +/* Baton type to be used with the interesting_merge callback. */ +typedef struct interesting_merge_baton_t +{ + /* What we are looking for. */ + svn_revnum_t rev; + svn_mergeinfo_t log_target_history_as_mergeinfo; + + /* Set to TRUE if we found it. */ + svn_boolean_t found_rev_of_interest; + + /* We need to invoke this user-provided callback if not NULL. */ + svn_repos_path_change_receiver_t inner; + void *inner_baton; +} interesting_merge_baton_t; + +/* Implements svn_repos_path_change_receiver_t. + * *BATON is a interesting_merge_baton_t. + * + * If BATON->REV a merged revision that is not already part of + * BATON->LOG_TARGET_HISTORY_AS_MERGEINFO, set BATON->FOUND_REV_OF_INTEREST. + */ +static svn_error_t * +interesting_merge(void *baton, + svn_repos_path_change_t *change, + apr_pool_t *scratch_pool) +{ + interesting_merge_baton_t *b = baton; + apr_hash_index_t *hi; + + if (b->inner) + SVN_ERR(b->inner(b->inner_baton, change, scratch_pool)); + + if (b->found_rev_of_interest) + return SVN_NO_ERROR; + + /* Look at each path on the log target's mergeinfo. */ + for (hi = apr_hash_first(scratch_pool, b->log_target_history_as_mergeinfo); + hi; + hi = apr_hash_next(hi)) + { + const char *mergeinfo_path = apr_hash_this_key(hi); + svn_rangelist_t *rangelist = apr_hash_this_val(hi); + + /* Check whether CHANGED_PATH at revision REV is a child of + a (path, revision) tuple in LOG_TARGET_HISTORY_AS_MERGEINFO. */ + if (svn_fspath__skip_ancestor(mergeinfo_path, change->path.data)) + { + int i; + + for (i = 0; i < rangelist->nelts; i++) + { + svn_merge_range_t *range + = APR_ARRAY_IDX(rangelist, i, svn_merge_range_t *); + if (b->rev > range->start && b->rev <= range->end) + return SVN_NO_ERROR; + } + } + } + + b->found_rev_of_interest = TRUE; + + return SVN_NO_ERROR; +} + +/* Send a log message for REV to the CALLBACKS. FS is used with REV to fetch the interesting history information, such as changed paths, revprops, etc. - The detect_changed function is used if either AUTHZ_READ_FUNC is - not NULL, or if DISCOVER_CHANGED_PATHS is TRUE. See it for details. + The detect_changed function is used if either CALLBACKS->AUTHZ_READ_FUNC + is not NULL, or if CALLBACKS->PATH_CHANGE_RECEIVER is not NULL. + See it for details. If DESCENDING_ORDER is true, send child messages in descending order. @@ -1227,107 +1305,51 @@ fill_log_entry(svn_log_entry_t *log_entry, static svn_error_t * send_log(svn_revnum_t rev, svn_fs_t *fs, - apr_hash_t *prefetched_changes, svn_mergeinfo_t log_target_history_as_mergeinfo, svn_bit_array__t *nested_merges, - svn_boolean_t discover_changed_paths, svn_boolean_t subtractive_merge, svn_boolean_t handling_merged_revision, const apr_array_header_t *revprops, svn_boolean_t has_children, - svn_log_entry_receiver_t receiver, - void *receiver_baton, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, + const log_callbacks_t *callbacks, apr_pool_t *pool) { - svn_log_entry_t *log_entry; - /* Assume we want to send the log for REV. */ - svn_boolean_t found_rev_of_interest = TRUE; + svn_repos_log_entry_t log_entry = { 0 }; + log_callbacks_t my_callbacks = *callbacks; - log_entry = svn_log_entry_create(pool); - SVN_ERR(fill_log_entry(log_entry, rev, fs, prefetched_changes, - discover_changed_paths || handling_merged_revision, - revprops, authz_read_func, authz_read_baton, pool)); - log_entry->has_children = has_children; - log_entry->subtractive_merge = subtractive_merge; + interesting_merge_baton_t baton; /* Is REV a merged revision that is already part of LOG_TARGET_HISTORY_AS_MERGEINFO? If so then there is no - need to send it, since it already was (or will be) sent. */ + need to send it, since it already was (or will be) sent. + + Use our callback to snoop through the changes. */ if (handling_merged_revision - && log_entry->changed_paths2 && log_target_history_as_mergeinfo && apr_hash_count(log_target_history_as_mergeinfo)) { - apr_hash_index_t *hi; - apr_pool_t *iterpool = svn_pool_create(pool); - - /* REV was merged in, but it might already be part of the log target's - natural history, so change our starting assumption. */ - found_rev_of_interest = FALSE; - - /* Look at each changed path in REV. */ - for (hi = apr_hash_first(pool, log_entry->changed_paths2); - hi; - hi = apr_hash_next(hi)) - { - svn_boolean_t path_is_in_history = FALSE; - const char *changed_path = apr_hash_this_key(hi); - apr_hash_index_t *hi2; - - /* Look at each path on the log target's mergeinfo. */ - for (hi2 = apr_hash_first(iterpool, - log_target_history_as_mergeinfo); - hi2; - hi2 = apr_hash_next(hi2)) - { - const char *mergeinfo_path = apr_hash_this_key(hi2); - svn_rangelist_t *rangelist = apr_hash_this_val(hi2); - - /* Check whether CHANGED_PATH at revision REV is a child of - a (path, revision) tuple in LOG_TARGET_HISTORY_AS_MERGEINFO. */ - if (svn_fspath__skip_ancestor(mergeinfo_path, changed_path)) - { - int i; - - for (i = 0; i < rangelist->nelts; i++) - { - svn_merge_range_t *range = - APR_ARRAY_IDX(rangelist, i, - svn_merge_range_t *); - if (rev > range->start && rev <= range->end) - { - path_is_in_history = TRUE; - break; - } - } - } - if (path_is_in_history) - break; - } - svn_pool_clear(iterpool); - - if (!path_is_in_history) - { - /* If even one path in LOG_ENTRY->CHANGED_PATHS2 is not part of - LOG_TARGET_HISTORY_AS_MERGEINFO, then we want to send the - log for REV. */ - found_rev_of_interest = TRUE; - break; - } - } - svn_pool_destroy(iterpool); + baton.found_rev_of_interest = FALSE; + baton.rev = rev; + baton.log_target_history_as_mergeinfo = log_target_history_as_mergeinfo; + baton.inner = callbacks->path_change_receiver; + baton.inner_baton = callbacks->path_change_receiver_baton; + + my_callbacks.path_change_receiver = interesting_merge; + my_callbacks.path_change_receiver_baton = &baton; + callbacks = &my_callbacks; + } + else + { + baton.found_rev_of_interest = TRUE; } - /* If we only got changed paths the sake of detecting redundant merged - revisions, then be sure we don't send that info to the receiver. */ - if (!discover_changed_paths && handling_merged_revision) - log_entry->changed_paths = log_entry->changed_paths2 = NULL; + SVN_ERR(fill_log_entry(&log_entry, rev, fs, revprops, callbacks, pool)); + log_entry.has_children = has_children; + log_entry.subtractive_merge = subtractive_merge; /* Send the entry to the receiver, unless it is a redundant merged revision. */ - if (found_rev_of_interest) + if (baton.found_rev_of_interest) { apr_pool_t *scratch_pool; @@ -1351,7 +1373,8 @@ send_log(svn_revnum_t rev, /* Pass a scratch pool to ensure no temporary state stored by the receiver callback persists. */ scratch_pool = svn_pool_create(pool); - SVN_ERR(receiver(receiver_baton, log_entry, scratch_pool)); + SVN_ERR(callbacks->revision_receiver(callbacks->revision_receiver_baton, + &log_entry, scratch_pool)); svn_pool_destroy(scratch_pool); } @@ -1700,7 +1723,6 @@ do_logs(svn_fs_t *fs, svn_revnum_t hist_start, svn_revnum_t hist_end, int limit, - svn_boolean_t discover_changed_paths, svn_boolean_t strict_node_history, svn_boolean_t include_merged_revisions, svn_boolean_t handling_merged_revisions, @@ -1708,10 +1730,7 @@ do_logs(svn_fs_t *fs, svn_boolean_t ignore_missing_locations, const apr_array_header_t *revprops, svn_boolean_t descending_order, - svn_log_entry_receiver_t receiver, - void *receiver_baton, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, + log_callbacks_t *callbacks, apr_pool_t *pool); /* Comparator function for handle_merged_revisions(). Sorts path_list_range @@ -1753,17 +1772,13 @@ handle_merged_revisions(svn_revnum_t rev, svn_mergeinfo_t processed, svn_mergeinfo_t added_mergeinfo, svn_mergeinfo_t deleted_mergeinfo, - svn_boolean_t discover_changed_paths, svn_boolean_t strict_node_history, const apr_array_header_t *revprops, - svn_log_entry_receiver_t receiver, - void *receiver_baton, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, + log_callbacks_t *callbacks, apr_pool_t *pool) { apr_array_header_t *combined_list = NULL; - svn_log_entry_t *empty_log_entry; + svn_repos_log_entry_t empty_log_entry = { 0 }; apr_pool_t *iterpool; int i; @@ -1794,17 +1809,16 @@ handle_merged_revisions(svn_revnum_t rev, SVN_ERR(do_logs(fs, pl_range->paths, log_target_history_as_mergeinfo, processed, nested_merges, pl_range->range.start, pl_range->range.end, 0, - discover_changed_paths, strict_node_history, + strict_node_history, TRUE, pl_range->reverse_merge, TRUE, TRUE, - revprops, TRUE, receiver, receiver_baton, - authz_read_func, authz_read_baton, iterpool)); + revprops, TRUE, callbacks, iterpool)); } svn_pool_destroy(iterpool); /* Send the empty revision. */ - empty_log_entry = svn_log_entry_create(pool); - empty_log_entry->revision = SVN_INVALID_REVNUM; - return (*receiver)(receiver_baton, empty_log_entry, pool); + empty_log_entry.revision = SVN_INVALID_REVNUM; + return (callbacks->revision_receiver)(callbacks->revision_receiver_baton, + &empty_log_entry, pool); } /* This is used by do_logs to differentiate between forward and @@ -1907,8 +1921,7 @@ store_search(svn_mergeinfo_t processed, const char *path = APR_ARRAY_IDX(paths, i, const char *); svn_rangelist_t *ranges = apr_array_make(processed_pool, 1, sizeof(svn_merge_range_t*)); - svn_merge_range_t *range = apr_palloc(processed_pool, - sizeof(svn_merge_range_t)); + svn_merge_range_t *range = apr_palloc(processed_pool, sizeof(*range)); range->start = start; range->end = end; @@ -1922,10 +1935,10 @@ store_search(svn_mergeinfo_t processed, return SVN_NO_ERROR; } -/* Find logs for PATHS from HIST_START to HIST_END in FS, and invoke - RECEIVER with RECEIVER_BATON on them. If DESCENDING_ORDER is TRUE, send - the logs back as we find them, else buffer the logs and send them back - in youngest->oldest order. +/* Find logs for PATHS from HIST_START to HIST_END in FS, and invoke the + CALLBACKS on them. If DESCENDING_ORDER is TRUE, send the logs back as + we find them, else buffer the logs and send them back in youngest->oldest + order. If IGNORE_MISSING_LOCATIONS is set, don't treat requests for bogus repository locations as fatal -- just ignore them. @@ -1935,7 +1948,7 @@ store_search(svn_mergeinfo_t processed, If HANDLING_MERGED_REVISIONS is TRUE then this is a recursive call for merged revisions, see INCLUDE_MERGED_REVISIONS argument to - svn_repos_get_logs4(). If SUBTRACTIVE_MERGE is true, then this is a + svn_repos_get_logs5(). If SUBTRACTIVE_MERGE is true, then this is a recursive call for reverse merged revisions. If NESTED_MERGES is not NULL then it is a hash of revisions (svn_revnum_t * @@ -1950,7 +1963,7 @@ store_search(svn_mergeinfo_t processed, revisions that have already been searched. Allocated like NESTED_MERGES above. - All other parameters are the same as svn_repos_get_logs4(). + All other parameters are the same as svn_repos_get_logs5(). */ static svn_error_t * do_logs(svn_fs_t *fs, @@ -1961,7 +1974,6 @@ do_logs(svn_fs_t *fs, svn_revnum_t hist_start, svn_revnum_t hist_end, int limit, - svn_boolean_t discover_changed_paths, svn_boolean_t strict_node_history, svn_boolean_t include_merged_revisions, svn_boolean_t subtractive_merge, @@ -1969,10 +1981,7 @@ do_logs(svn_fs_t *fs, svn_boolean_t ignore_missing_locations, const apr_array_header_t *revprops, svn_boolean_t descending_order, - svn_log_entry_receiver_t receiver, - void *receiver_baton, - svn_repos_authz_func_t authz_read_func, - void *authz_read_baton, + log_callbacks_t *callbacks, apr_pool_t *pool) { apr_pool_t *iterpool, *iterpool2; @@ -2005,7 +2014,8 @@ do_logs(svn_fs_t *fs, revisions contain real changes to at least one of our paths. */ SVN_ERR(get_path_histories(&histories, fs, paths, hist_start, hist_end, strict_node_history, ignore_missing_locations, - authz_read_func, authz_read_baton, pool)); + callbacks->authz_read_func, + callbacks->authz_read_baton, pool)); /* Loop through all the revisions in the range and add any where a path was changed to the array, or if they wanted @@ -2029,9 +2039,10 @@ do_logs(svn_fs_t *fs, /* Check history for this path in current rev. */ SVN_ERR(check_history(&changed, info, fs, current, - strict_node_history, authz_read_func, - authz_read_baton, hist_start, pool, - iterpool2)); + strict_node_history, + callbacks->authz_read_func, + callbacks->authz_read_baton, + hist_start, pool, iterpool2)); if (! info->done) any_histories_left = TRUE; } @@ -2044,7 +2055,6 @@ do_logs(svn_fs_t *fs, svn_mergeinfo_t added_mergeinfo = NULL; svn_mergeinfo_t deleted_mergeinfo = NULL; svn_boolean_t has_children = FALSE; - apr_hash_t *changes = NULL; /* If we're including merged revisions, we need to calculate the mergeinfo deltas committed in this revision to our @@ -2065,7 +2075,6 @@ do_logs(svn_fs_t *fs, } SVN_ERR(get_combined_mergeinfo_changes(&added_mergeinfo, &deleted_mergeinfo, - &changes, fs, cur_paths, current, iterpool, iterpool)); @@ -2078,13 +2087,10 @@ do_logs(svn_fs_t *fs, in anyway). */ if (descending_order) { - SVN_ERR(send_log(current, fs, changes, + SVN_ERR(send_log(current, fs, log_target_history_as_mergeinfo, nested_merges, - discover_changed_paths, subtractive_merge, handling_merged_revisions, - revprops, has_children, - receiver, receiver_baton, - authz_read_func, authz_read_baton, iterpool)); + revprops, has_children, callbacks, iterpool)); if (has_children) /* Implies include_merged_revisions == TRUE */ { @@ -2103,12 +2109,9 @@ do_logs(svn_fs_t *fs, log_target_history_as_mergeinfo, nested_merges, processed, added_mergeinfo, deleted_mergeinfo, - discover_changed_paths, strict_node_history, revprops, - receiver, receiver_baton, - authz_read_func, - authz_read_baton, + callbacks, iterpool)); } if (limit && ++send_count >= limit) @@ -2176,20 +2179,17 @@ do_logs(svn_fs_t *fs, if (rev_mergeinfo) { struct added_deleted_mergeinfo *add_and_del_mergeinfo = - apr_hash_get(rev_mergeinfo, ¤t, sizeof(svn_revnum_t)); + apr_hash_get(rev_mergeinfo, ¤t, sizeof(current)); added_mergeinfo = add_and_del_mergeinfo->added_mergeinfo; deleted_mergeinfo = add_and_del_mergeinfo->deleted_mergeinfo; has_children = (apr_hash_count(added_mergeinfo) > 0 || apr_hash_count(deleted_mergeinfo) > 0); } - SVN_ERR(send_log(current, fs, NULL, + SVN_ERR(send_log(current, fs, log_target_history_as_mergeinfo, nested_merges, - discover_changed_paths, subtractive_merge, - handling_merged_revisions, - revprops, has_children, - receiver, receiver_baton, authz_read_func, - authz_read_baton, iterpool)); + subtractive_merge, handling_merged_revisions, + revprops, has_children, callbacks, iterpool)); if (has_children) { if (!nested_merges) @@ -2204,12 +2204,8 @@ do_logs(svn_fs_t *fs, processed, added_mergeinfo, deleted_mergeinfo, - discover_changed_paths, strict_node_history, - revprops, - receiver, receiver_baton, - authz_read_func, - authz_read_baton, + revprops, callbacks, iterpool)); } if (limit && i + 1 >= limit) @@ -2227,7 +2223,7 @@ struct location_segment_baton apr_pool_t *pool; }; -/* svn_location_segment_receiver_t implementation for svn_repos_get_logs4. */ +/* svn_location_segment_receiver_t implementation for svn_repos_get_logs5. */ static svn_error_t * location_segment_receiver(svn_location_segment_t *segment, void *baton, @@ -2247,7 +2243,7 @@ location_segment_receiver(svn_location_segment_t *segment, filesystem. START_REV and END_REV must be valid revisions. RESULT_POOL is used to allocate *PATHS_HISTORY_MERGEINFO, SCRATCH_POOL is used for all other (temporary) allocations. Other parameters are the same as - svn_repos_get_logs4(). */ + svn_repos_get_logs5(). */ static svn_error_t * get_paths_history_as_mergeinfo(svn_mergeinfo_t *paths_history_mergeinfo, svn_repos_t *repos, @@ -2308,41 +2304,56 @@ get_paths_history_as_mergeinfo(svn_mergeinfo_t *paths_history_mergeinfo, } svn_error_t * -svn_repos_get_logs4(svn_repos_t *repos, +svn_repos_get_logs5(svn_repos_t *repos, const apr_array_header_t *paths, svn_revnum_t start, svn_revnum_t end, int limit, - svn_boolean_t discover_changed_paths, svn_boolean_t strict_node_history, svn_boolean_t include_merged_revisions, const apr_array_header_t *revprops, svn_repos_authz_func_t authz_read_func, void *authz_read_baton, - svn_log_entry_receiver_t receiver, - void *receiver_baton, - apr_pool_t *pool) + svn_repos_path_change_receiver_t path_change_receiver, + void *path_change_receiver_baton, + svn_repos_log_entry_receiver_t revision_receiver, + void *revision_receiver_baton, + apr_pool_t *scratch_pool) { svn_revnum_t head = SVN_INVALID_REVNUM; svn_fs_t *fs = repos->fs; svn_boolean_t descending_order; svn_mergeinfo_t paths_history_mergeinfo = NULL; + log_callbacks_t callbacks; + + callbacks.path_change_receiver = path_change_receiver; + callbacks.path_change_receiver_baton = path_change_receiver_baton; + callbacks.revision_receiver = revision_receiver; + callbacks.revision_receiver_baton = revision_receiver_baton; + callbacks.authz_read_func = authz_read_func; + callbacks.authz_read_baton = authz_read_baton; if (revprops) { int i; apr_array_header_t *new_revprops - = apr_array_make(pool, revprops->nelts, sizeof(svn_string_t *)); + = apr_array_make(scratch_pool, revprops->nelts, + sizeof(svn_string_t *)); for (i = 0; i < revprops->nelts; ++i) APR_ARRAY_PUSH(new_revprops, svn_string_t *) - = svn_string_create(APR_ARRAY_IDX(revprops, i, const char *), pool); + = svn_string_create(APR_ARRAY_IDX(revprops, i, const char *), + scratch_pool); revprops = new_revprops; } + /* Make sure we catch up on the latest revprop changes. This is the only + * time we will refresh the revprop data in this query. */ + SVN_ERR(svn_fs_refresh_revision_props(fs, scratch_pool)); + /* Setup log range. */ - SVN_ERR(svn_fs_youngest_rev(&head, fs, pool)); + SVN_ERR(svn_fs_youngest_rev(&head, fs, scratch_pool)); if (! SVN_IS_VALID_REVNUM(start)) start = head; @@ -2371,7 +2382,7 @@ svn_repos_get_logs4(svn_repos_t *repos, } if (! paths) - paths = apr_array_make(pool, 0, sizeof(const char *)); + paths = apr_array_make(scratch_pool, 0, sizeof(const char *)); /* If we're not including merged revisions, and we were given no paths or a single empty (or "/") path, then we can bypass a bunch @@ -2386,7 +2397,7 @@ svn_repos_get_logs4(svn_repos_t *repos, { apr_uint64_t send_count = 0; int i; - apr_pool_t *iterpool = svn_pool_create(pool); + apr_pool_t *iterpool = svn_pool_create(scratch_pool); /* If we are provided an authz callback function, use it to verify that the user has read access to the root path in the @@ -2402,9 +2413,10 @@ svn_repos_get_logs4(svn_repos_t *repos, svn_fs_root_t *rev_root; SVN_ERR(svn_fs_revision_root(&rev_root, fs, - descending_order ? end : start, pool)); + descending_order ? end : start, + scratch_pool)); SVN_ERR(authz_read_func(&readable, rev_root, "", - authz_read_baton, pool)); + authz_read_baton, scratch_pool)); if (! readable) return svn_error_create(SVN_ERR_AUTHZ_UNREADABLE, NULL, NULL); } @@ -2422,10 +2434,9 @@ svn_repos_get_logs4(svn_repos_t *repos, rev = end - i; else rev = start + i; - SVN_ERR(send_log(rev, fs, NULL, NULL, NULL, - discover_changed_paths, FALSE, - FALSE, revprops, FALSE, receiver, receiver_baton, - authz_read_func, authz_read_baton, iterpool)); + SVN_ERR(send_log(rev, fs, NULL, NULL, + FALSE, FALSE, revprops, FALSE, + &callbacks, iterpool)); } svn_pool_destroy(iterpool); @@ -2439,19 +2450,18 @@ svn_repos_get_logs4(svn_repos_t *repos, http://subversion.tigris.org/issues/show_bug.cgi?id=3650#desc5 */ if (include_merged_revisions) { - apr_pool_t *subpool = svn_pool_create(pool); + apr_pool_t *subpool = svn_pool_create(scratch_pool); SVN_ERR(get_paths_history_as_mergeinfo(&paths_history_mergeinfo, repos, paths, start, end, authz_read_func, authz_read_baton, - pool, subpool)); + scratch_pool, subpool)); svn_pool_destroy(subpool); } - return do_logs(repos->fs, paths, paths_history_mergeinfo, NULL, NULL, start, end, - limit, discover_changed_paths, strict_node_history, + return do_logs(repos->fs, paths, paths_history_mergeinfo, NULL, NULL, + start, end, limit, strict_node_history, include_merged_revisions, FALSE, FALSE, FALSE, - revprops, descending_order, receiver, receiver_baton, - authz_read_func, authz_read_baton, pool); + revprops, descending_order, &callbacks, scratch_pool); } diff --git a/subversion/libsvn_repos/replay.c b/subversion/libsvn_repos/replay.c index bcf260c47a04..9bcc667f6da4 100644 --- a/subversion/libsvn_repos/replay.c +++ b/subversion/libsvn_repos/replay.c @@ -198,7 +198,7 @@ add_subdir(svn_fs_root_t *source_root, for (hi = apr_hash_first(pool, dirents); hi; hi = apr_hash_next(hi)) { - svn_fs_path_change2_t *change; + svn_fs_path_change3_t *change; svn_boolean_t readable = TRUE; svn_fs_dirent_t *dent = apr_hash_this_val(hi); const char *copyfrom_path = NULL; @@ -412,7 +412,7 @@ fill_copyfrom(svn_fs_root_t **copyfrom_root, svn_revnum_t *copyfrom_rev, svn_boolean_t *src_readable, svn_fs_root_t *root, - svn_fs_path_change2_t *change, + svn_fs_path_change3_t *change, svn_repos_authz_func_t authz_read_func, void *authz_read_baton, const char *path, @@ -463,7 +463,7 @@ path_driver_cb_func(void **dir_baton, const svn_delta_editor_t *editor = cb->editor; void *edit_baton = cb->edit_baton; svn_fs_root_t *root = cb->root; - svn_fs_path_change2_t *change; + svn_fs_path_change3_t *change; svn_boolean_t do_add = FALSE, do_delete = FALSE; void *file_baton = NULL; svn_revnum_t copyfrom_rev; @@ -555,10 +555,6 @@ path_driver_cb_func(void **dir_baton, return svn_error_create(SVN_ERR_FS_ALREADY_EXISTS, NULL, _("Root directory already exists.")); - /* A NULL parent_baton will cause a segfault. It should never be - NULL for non-root paths. */ - SVN_ERR_ASSERT(parent_baton); - /* Was this node copied? */ SVN_ERR(fill_copyfrom(©from_root, ©from_path, ©from_rev, &src_readable, root, change, @@ -847,6 +843,80 @@ fetch_props_func(apr_hash_t **props, +/* Retrieve the path changes under ROOT, filter them with AUTHZ_READ_FUNC + and AUTHZ_READ_BATON and return those that intersect with BASE_RELPATH. + + The svn_fs_path_change3_t* will be returned in *CHANGED_PATHS, keyed by + their path. The paths themselves are additionally returned in *PATHS. + + Allocate the returned data in RESULT_POOL and use SCRATCH_POOL for + temporary allocations. + */ +static svn_error_t * +get_relevant_changes(apr_hash_t **changed_paths, + apr_array_header_t **paths, + svn_fs_root_t *root, + const char *base_relpath, + svn_repos_authz_func_t authz_read_func, + void *authz_read_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_fs_path_change_iterator_t *iterator; + svn_fs_path_change3_t *change; + apr_pool_t *iterpool = svn_pool_create(scratch_pool); + + /* Fetch the paths changed under ROOT. */ + SVN_ERR(svn_fs_paths_changed3(&iterator, root, scratch_pool, scratch_pool)); + SVN_ERR(svn_fs_path_change_get(&change, iterator)); + + /* Make an array from the keys of our CHANGED_PATHS hash, and copy + the values into a new hash whose keys have no leading slashes. */ + *paths = apr_array_make(result_pool, 16, sizeof(const char *)); + *changed_paths = apr_hash_make(result_pool); + while (change) + { + const char *path = change->path.data; + apr_ssize_t keylen = change->path.len; + svn_boolean_t allowed = TRUE; + + svn_pool_clear(iterpool); + if (authz_read_func) + SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, + iterpool)); + + if (allowed) + { + if (path[0] == '/') + { + path++; + keylen--; + } + + /* If the base_path doesn't match the top directory of this path + we don't want anything to do with it... + ...unless this was a change to one of the parent directories of + base_path. */ + if ( svn_relpath_skip_ancestor(base_relpath, path) + || svn_relpath_skip_ancestor(path, base_relpath)) + { + change = svn_fs_path_change3_dup(change, result_pool); + path = change->path.data; + if (path[0] == '/') + path++; + + APR_ARRAY_PUSH(*paths, const char *) = path; + apr_hash_set(*changed_paths, path, keylen, change); + } + } + + SVN_ERR(svn_fs_path_change_get(&change, iterator)); + } + + svn_pool_destroy(iterpool); + return SVN_NO_ERROR; +} + svn_error_t * svn_repos_replay2(svn_fs_root_t *root, const char *base_path, @@ -859,9 +929,7 @@ svn_repos_replay2(svn_fs_root_t *root, apr_pool_t *pool) { #ifndef USE_EV2_IMPL - apr_hash_t *fs_changes; apr_hash_t *changed_paths; - apr_hash_index_t *hi; apr_array_header_t *paths; struct path_driver_cb_baton cb_baton; @@ -873,54 +941,15 @@ svn_repos_replay2(svn_fs_root_t *root, return SVN_NO_ERROR; } - /* Fetch the paths changed under ROOT. */ - SVN_ERR(svn_fs_paths_changed2(&fs_changes, root, pool)); - if (! base_path) base_path = ""; else if (base_path[0] == '/') ++base_path; - /* Make an array from the keys of our CHANGED_PATHS hash, and copy - the values into a new hash whose keys have no leading slashes. */ - paths = apr_array_make(pool, apr_hash_count(fs_changes), - sizeof(const char *)); - changed_paths = apr_hash_make(pool); - for (hi = apr_hash_first(pool, fs_changes); hi; hi = apr_hash_next(hi)) - { - const char *path = apr_hash_this_key(hi); - apr_ssize_t keylen = apr_hash_this_key_len(hi); - svn_fs_path_change2_t *change = apr_hash_this_val(hi); - svn_boolean_t allowed = TRUE; - - if (authz_read_func) - SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, - pool)); - - if (allowed) - { - if (path[0] == '/') - { - path++; - keylen--; - } - - /* If the base_path doesn't match the top directory of this path - we don't want anything to do with it... */ - if (svn_relpath_skip_ancestor(base_path, path) != NULL) - { - APR_ARRAY_PUSH(paths, const char *) = path; - apr_hash_set(changed_paths, path, keylen, change); - } - /* ...unless this was a change to one of the parent directories of - base_path. */ - else if (svn_relpath_skip_ancestor(path, base_path) != NULL) - { - APR_ARRAY_PUSH(paths, const char *) = path; - apr_hash_set(changed_paths, path, keylen, change); - } - } - } + /* Fetch the paths changed under ROOT. */ + SVN_ERR(get_relevant_changes(&changed_paths, &paths, root, base_path, + authz_read_func, authz_read_baton, + pool, pool)); /* If we were not given a low water mark, assume that everything is there, all the way back to revision 0. */ @@ -971,6 +1000,11 @@ svn_repos_replay2(svn_fs_root_t *root, const char *repos_root = ""; void *unlock_baton; + /* If we were not given a low water mark, assume that everything is there, + all the way back to revision 0. */ + if (! SVN_IS_VALID_REVNUM(low_water_mark)) + low_water_mark = 0; + /* Special-case r0, which we know is an empty revision; if we don't special-case it we might end up trying to compare it to "r-1". */ if (svn_fs_is_revision_root(root) @@ -1061,7 +1095,7 @@ add_subdir_ev2(svn_fs_root_t *source_root, for (hi = apr_hash_first(scratch_pool, dirents); hi; hi = apr_hash_next(hi)) { - svn_fs_path_change2_t *change; + svn_fs_path_change3_t *change; svn_boolean_t readable = TRUE; svn_fs_dirent_t *dent = apr_hash_this_val(hi); const char *copyfrom_path = NULL; @@ -1189,7 +1223,7 @@ replay_node(svn_fs_root_t *root, apr_pool_t *result_pool, apr_pool_t *scratch_pool) { - svn_fs_path_change2_t *change; + svn_fs_path_change3_t *change; svn_boolean_t do_add = FALSE; svn_boolean_t do_delete = FALSE; svn_revnum_t copyfrom_rev; @@ -1484,16 +1518,14 @@ svn_repos__replay_ev2(svn_fs_root_t *root, void *authz_read_baton, apr_pool_t *scratch_pool) { - apr_hash_t *fs_changes; apr_hash_t *changed_paths; - apr_hash_index_t *hi; apr_array_header_t *paths; apr_array_header_t *copies; apr_pool_t *iterpool; svn_error_t *err = SVN_NO_ERROR; int i; - SVN_ERR_ASSERT(!svn_dirent_is_absolute(base_repos_relpath)); + SVN_ERR_ASSERT(svn_relpath_is_canonical(base_repos_relpath)); /* Special-case r0, which we know is an empty revision; if we don't special-case it we might end up trying to compare it to "r-1". */ @@ -1504,49 +1536,10 @@ svn_repos__replay_ev2(svn_fs_root_t *root, } /* Fetch the paths changed under ROOT. */ - SVN_ERR(svn_fs_paths_changed2(&fs_changes, root, scratch_pool)); - - /* Make an array from the keys of our CHANGED_PATHS hash, and copy - the values into a new hash whose keys have no leading slashes. */ - paths = apr_array_make(scratch_pool, apr_hash_count(fs_changes), - sizeof(const char *)); - changed_paths = apr_hash_make(scratch_pool); - for (hi = apr_hash_first(scratch_pool, fs_changes); hi; - hi = apr_hash_next(hi)) - { - const char *path = apr_hash_this_key(hi); - apr_ssize_t keylen = apr_hash_this_key_len(hi); - svn_fs_path_change2_t *change = apr_hash_this_val(hi); - svn_boolean_t allowed = TRUE; - - if (authz_read_func) - SVN_ERR(authz_read_func(&allowed, root, path, authz_read_baton, - scratch_pool)); - - if (allowed) - { - if (path[0] == '/') - { - path++; - keylen--; - } - - /* If the base_path doesn't match the top directory of this path - we don't want anything to do with it... */ - if (svn_relpath_skip_ancestor(base_repos_relpath, path) != NULL) - { - APR_ARRAY_PUSH(paths, const char *) = path; - apr_hash_set(changed_paths, path, keylen, change); - } - /* ...unless this was a change to one of the parent directories of - base_path. */ - else if (svn_relpath_skip_ancestor(path, base_repos_relpath) != NULL) - { - APR_ARRAY_PUSH(paths, const char *) = path; - apr_hash_set(changed_paths, path, keylen, change); - } - } - } + SVN_ERR(get_relevant_changes(&changed_paths, &paths, root, + base_repos_relpath, + authz_read_func, authz_read_baton, + scratch_pool, scratch_pool)); /* If we were not given a low water mark, assume that everything is there, all the way back to revision 0. */ diff --git a/subversion/libsvn_repos/reporter.c b/subversion/libsvn_repos/reporter.c index 76c72016144c..f56cb2ee980c 100644 --- a/subversion/libsvn_repos/reporter.c +++ b/subversion/libsvn_repos/reporter.c @@ -481,10 +481,12 @@ get_revision_info(report_baton_t *b, { /* Info is not available, yet. Get all revprops. */ - SVN_ERR(svn_fs_revision_proplist(&r_props, - b->repos->fs, - rev, - scratch_pool)); + SVN_ERR(svn_fs_revision_proplist2(&r_props, + b->repos->fs, + rev, + FALSE, + scratch_pool, + scratch_pool)); /* Extract the committed-date. */ cdate = svn_hash_gets(r_props, SVN_PROP_REVISION_DATE); @@ -520,19 +522,13 @@ delta_proplists(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, { svn_fs_root_t *s_root; apr_hash_t *s_props = NULL, *t_props; - apr_array_header_t *prop_diffs; - int i; svn_revnum_t crev; - revision_info_t *revision_info; - svn_boolean_t changed; - const svn_prop_t *pc; - svn_lock_t *lock; - apr_hash_index_t *hi; /* Fetch the created-rev and send entry props. */ SVN_ERR(svn_fs_node_created_rev(&crev, b->t_root, t_path, pool)); if (SVN_IS_VALID_REVNUM(crev)) { + revision_info_t *revision_info; /* convert committed-rev to string */ char buf[SVN_INT64_BUFFER_SIZE]; svn_string_t cr_str; @@ -563,6 +559,7 @@ delta_proplists(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, /* Update lock properties. */ if (lock_token) { + svn_lock_t *lock; SVN_ERR(svn_fs_get_lock(&lock, b->repos->fs, t_path, pool)); /* Delete a defunct lock. */ @@ -573,6 +570,7 @@ delta_proplists(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, if (s_path) { + svn_boolean_t changed; SVN_ERR(get_source_root(b, &s_root, s_rev)); /* Is this deltification worth our time? */ @@ -590,16 +588,20 @@ delta_proplists(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, if (s_props && apr_hash_count(s_props)) { + apr_array_header_t *prop_diffs; + int i; + /* Now transmit the differences. */ SVN_ERR(svn_prop_diffs(&prop_diffs, t_props, s_props, pool)); for (i = 0; i < prop_diffs->nelts; i++) { - pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t); + const svn_prop_t *pc = &APR_ARRAY_IDX(prop_diffs, i, svn_prop_t); SVN_ERR(change_fn(b, object, pc->name, pc->value, pool)); } } else if (apr_hash_count(t_props)) { + apr_hash_index_t *hi; /* So source, i.e. all new. Transmit all target props. */ for (hi = apr_hash_first(pool, t_props); hi; hi = apr_hash_next(hi)) { @@ -669,7 +671,6 @@ delta_files(report_baton_t *b, void *file_baton, svn_revnum_t s_rev, const char *s_path, const char *t_path, const char *lock_token, apr_pool_t *pool) { - svn_boolean_t changed; svn_fs_root_t *s_root = NULL; svn_txdelta_stream_t *dstream = NULL; svn_checksum_t *s_checksum; @@ -683,14 +684,15 @@ delta_files(report_baton_t *b, void *file_baton, svn_revnum_t s_rev, if (s_path) { + svn_boolean_t changed; SVN_ERR(get_source_root(b, &s_root, s_rev)); /* We're not interested in the theoretical difference between "has contents which have not changed with respect to" and "has the same actual contents as" when sending text-deltas. If we know the delta is an empty one, we avoiding sending it in either case. */ - SVN_ERR(svn_repos__compare_files(&changed, b->t_root, t_path, - s_root, s_path, pool)); + SVN_ERR(svn_fs_contents_different(&changed, b->t_root, t_path, + s_root, s_path, pool)); if (!changed) return SVN_NO_ERROR; @@ -971,11 +973,11 @@ update_entry(report_baton_t *b, svn_revnum_t s_rev, const char *s_path, if (s_root == NULL) SVN_ERR(get_source_root(b, &s_root, s_rev)); - SVN_ERR(svn_fs_props_different(&changed, s_root, s_path, - b->t_root, t_path, pool)); + SVN_ERR(svn_fs_props_changed(&changed, s_root, s_path, + b->t_root, t_path, pool)); if (!changed) - SVN_ERR(svn_fs_contents_different(&changed, s_root, s_path, - b->t_root, t_path, pool)); + SVN_ERR(svn_fs_contents_changed(&changed, s_root, s_path, + b->t_root, t_path, pool)); } if ((distance == 0 || !changed) && !any_path_info(b, e_path) @@ -1567,6 +1569,7 @@ svn_repos_finish_report(void *baton, apr_pool_t *pool) { report_baton_t *b = baton; + SVN_ERR(svn_fs_refresh_revision_props(svn_repos_fs(b->repos), pool)); return svn_error_trace(finish_report(b, pool)); } diff --git a/subversion/libsvn_repos/repos.c b/subversion/libsvn_repos/repos.c index 1d62aeb501f1..2333f561efda 100644 --- a/subversion/libsvn_repos/repos.c +++ b/subversion/libsvn_repos/repos.c @@ -393,15 +393,15 @@ create_hooks(svn_repos_t *repos, apr_pool_t *pool) "# e.g.: \"" SVN_RA_CAPABILITY_MERGEINFO ":some-other-capability\" " \ "(the order is undefined)." NL "#" NL +"# The list is self-reported by the client. Therefore, you should not" NL +"# make security assumptions based on the capabilities list, nor should" NL +"# you assume that clients reliably report every capability they have." NL +"#" NL "# Note: The TXN-NAME parameter is new in Subversion 1.8. Prior to version" NL "# 1.8, the start-commit hook was invoked before the commit txn was even" NL "# created, so the ability to inspect the commit txn and its metadata from" NL "# within the start-commit hook was not possible." NL "# " NL -"# The list is self-reported by the client. Therefore, you should not" NL -"# make security assumptions based on the capabilities list, nor should" NL -"# you assume that clients reliably report every capability they have." NL -"#" NL "# If the hook program exits with success, the commit continues; but" NL "# if it exits with failure (non-zero), the commit is stopped before" NL "# a Subversion txn is created, and STDERR is returned to the client." NL; @@ -882,7 +882,7 @@ create_conf(svn_repos_t *repos, apr_pool_t *pool) "[sasl]" NL "### This option specifies whether you want to use the Cyrus SASL" NL "### library for authentication. Default is false." NL -"### This section will be ignored if svnserve is not built with Cyrus" NL +"### Enabling this option requires svnserve to have been built with Cyrus" NL "### SASL support; to check, run 'svnserve --version' and look for a line" NL "### reading 'Cyrus SASL authentication is available.'" NL "# use-sasl = true" NL @@ -1180,8 +1180,8 @@ svn_repos_create(svn_repos_t **repos_p, SVN_ERR(lock_repos(repos, FALSE, FALSE, scratch_pool)); /* Create an environment for the filesystem. */ - if ((err = svn_fs_create(&repos->fs, repos->db_path, fs_config, - result_pool))) + if ((err = svn_fs_create2(&repos->fs, repos->db_path, fs_config, + result_pool, scratch_pool))) { /* If there was an error making the filesytem, e.g. unknown/supported * filesystem type. Clean up after ourselves. Yes this is safe because @@ -1504,6 +1504,16 @@ static const char *capability_yes = "yes"; /* Repository does not support the capability. */ static const char *capability_no = "no"; +static svn_error_t * +dummy_mergeinfo_receiver(const char *path, + svn_mergeinfo_t mergeinfo, + void *baton, + apr_pool_t *scratch_pool) +{ + return SVN_NO_ERROR; +} + + svn_error_t * svn_repos_has_capability(svn_repos_t *repos, svn_boolean_t *has, @@ -1525,14 +1535,13 @@ svn_repos_has_capability(svn_repos_t *repos, { svn_error_t *err; svn_fs_root_t *root; - svn_mergeinfo_catalog_t ignored; apr_array_header_t *paths = apr_array_make(pool, 1, sizeof(char *)); SVN_ERR(svn_fs_revision_root(&root, repos->fs, 0, pool)); APR_ARRAY_PUSH(paths, const char *) = ""; - err = svn_fs_get_mergeinfo2(&ignored, root, paths, FALSE, FALSE, - TRUE, pool, pool); + err = svn_fs_get_mergeinfo3(root, paths, FALSE, FALSE, TRUE, + dummy_mergeinfo_receiver, NULL, pool); if (err) { @@ -2061,45 +2070,6 @@ svn_repos_version(void) SVN_VERSION_BODY; } - - -svn_error_t * -svn_repos_stat(svn_dirent_t **dirent, - svn_fs_root_t *root, - const char *path, - apr_pool_t *pool) -{ - svn_node_kind_t kind; - svn_dirent_t *ent; - const char *datestring; - - SVN_ERR(svn_fs_check_path(&kind, root, path, pool)); - - if (kind == svn_node_none) - { - *dirent = NULL; - return SVN_NO_ERROR; - } - - ent = svn_dirent_create(pool); - ent->kind = kind; - - if (kind == svn_node_file) - SVN_ERR(svn_fs_file_length(&(ent->size), root, path, pool)); - - SVN_ERR(svn_fs_node_has_props(&ent->has_props, root, path, pool)); - - SVN_ERR(svn_repos_get_committed_info(&(ent->created_rev), - &datestring, - &(ent->last_author), - root, path, pool)); - if (datestring) - SVN_ERR(svn_time_from_cstring(&(ent->time), datestring, pool)); - - *dirent = ent; - return SVN_NO_ERROR; -} - svn_error_t * svn_repos_remember_client_capabilities(svn_repos_t *repos, const apr_array_header_t *capabilities) diff --git a/subversion/libsvn_repos/repos.h b/subversion/libsvn_repos/repos.h index b1039acd8d9f..2274032dd769 100644 --- a/subversion/libsvn_repos/repos.h +++ b/subversion/libsvn_repos/repos.h @@ -27,6 +27,7 @@ #include <apr_hash.h> #include "svn_fs.h" +#include "svn_config.h" #ifdef __cplusplus extern "C" { @@ -362,45 +363,8 @@ svn_repos__hooks_post_unlock(svn_repos_t *repos, apr_pool_t *pool); -/*** Authz Functions ***/ - -/* Read authz configuration data from PATH into *AUTHZ_P, allocated - in POOL. If GROUPS_PATH is set, use the global groups parsed from it. - - PATH and GROUPS_PATH may be a dirent or a registry path and iff ACCEPT_URLS - is set it may also be an absolute file url. - - If PATH or GROUPS_PATH is not a valid authz rule file, then return - SVN_AUTHZ_INVALID_CONFIG. The contents of *AUTHZ_P is then - undefined. If MUST_EXIST is TRUE, a missing authz or global groups file - is also an error. */ -svn_error_t * -svn_repos__authz_read(svn_authz_t **authz_p, - const char *path, - const char *groups_path, - svn_boolean_t must_exist, - svn_boolean_t accept_urls, - apr_pool_t *pool); - -/* Walk the configuration in AUTHZ looking for any errors. */ -svn_error_t * -svn_repos__authz_validate(svn_authz_t *authz, - apr_pool_t *pool); - - /*** Utility Functions ***/ -/* Set *CHANGED_P to TRUE if ROOT1/PATH1 and ROOT2/PATH2 have - different contents, FALSE if they have the same contents. - Use POOL for temporary allocation. */ -svn_error_t * -svn_repos__compare_files(svn_boolean_t *changed_p, - svn_fs_root_t *root1, - const char *path1, - svn_fs_root_t *root2, - const char *path2, - apr_pool_t *pool); - /* Set *PREV_PATH and *PREV_REV to the path and revision which represent the location at which PATH in FS was located immediately prior to REVISION iff there was a copy operation (to PATH or one of diff --git a/subversion/libsvn_repos/rev_hunt.c b/subversion/libsvn_repos/rev_hunt.c index d6cc49576d12..558d691b9bfe 100644 --- a/subversion/libsvn_repos/rev_hunt.c +++ b/subversion/libsvn_repos/rev_hunt.c @@ -44,7 +44,7 @@ /* Note: this binary search assumes that the datestamp properties on each revision are in chronological order. That is if revision A > - revision B, then A's datestamp is younger then B's datestamp. + revision B, then A's datestamp is younger than B's datestamp. If someone comes along and sets a bogus datestamp, this routine might not work right. @@ -65,8 +65,8 @@ get_time(apr_time_t *tm, { svn_string_t *date_str; - SVN_ERR(svn_fs_revision_prop(&date_str, fs, rev, SVN_PROP_REVISION_DATE, - pool)); + SVN_ERR(svn_fs_revision_prop2(&date_str, fs, rev, SVN_PROP_REVISION_DATE, + FALSE, pool, pool)); if (! date_str) return svn_error_createf (SVN_ERR_FS_GENERAL, NULL, @@ -88,6 +88,7 @@ svn_repos_dated_revision(svn_revnum_t *revision, /* Initialize top and bottom values of binary search. */ SVN_ERR(svn_fs_youngest_rev(&rev_latest, fs, pool)); + SVN_ERR(svn_fs_refresh_revision_props(fs, pool)); rev_bot = 0; rev_top = rev_latest; @@ -170,7 +171,8 @@ svn_repos_get_committed_info(svn_revnum_t *committed_rev, SVN_ERR(svn_fs_node_created_rev(committed_rev, root, path, pool)); /* Get the revision properties of this revision. */ - SVN_ERR(svn_fs_revision_proplist(&revprops, fs, *committed_rev, pool)); + SVN_ERR(svn_fs_revision_proplist2(&revprops, fs, *committed_rev, TRUE, + pool, pool)); /* Extract date and author from these revprops. */ committed_date_s = svn_hash_gets(revprops, SVN_PROP_REVISION_DATE); @@ -669,8 +671,7 @@ svn_repos_trace_node_locations(svn_fs_t *fs, /* First - let's sort the array of the revisions from the greatest revision * downward, so it will be easier to search on. */ location_revisions = apr_array_copy(pool, location_revisions_orig); - qsort(location_revisions->elts, location_revisions->nelts, - sizeof(*revision_ptr), svn_sort_compare_revisions); + svn_sort__array(location_revisions, svn_sort_compare_revisions); revision_ptr = (svn_revnum_t *)location_revisions->elts; revision_ptr_end = revision_ptr + location_revisions->nelts; @@ -835,27 +836,32 @@ svn_repos_node_location_segments(svn_repos_t *repos, { svn_fs_t *fs = svn_repos_fs(repos); svn_stringbuf_t *current_path; - svn_revnum_t youngest_rev = SVN_INVALID_REVNUM, current_rev; + svn_revnum_t youngest_rev, current_rev; apr_pool_t *subpool; + SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); + /* No PEG_REVISION? We'll use HEAD. */ if (! SVN_IS_VALID_REVNUM(peg_revision)) - { - SVN_ERR(svn_fs_youngest_rev(&youngest_rev, fs, pool)); - peg_revision = youngest_rev; - } + peg_revision = youngest_rev; + + if (peg_revision > youngest_rev) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("No such revision %ld"), peg_revision); - /* No START_REV? We'll use HEAD (which we may have already fetched). */ + /* No START_REV? We'll use peg rev. */ if (! SVN_IS_VALID_REVNUM(start_rev)) - { - if (SVN_IS_VALID_REVNUM(youngest_rev)) - start_rev = youngest_rev; - else - SVN_ERR(svn_fs_youngest_rev(&start_rev, fs, pool)); - } + start_rev = peg_revision; + else if (start_rev > peg_revision) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("No such revision %ld"), start_rev); /* No END_REV? We'll use 0. */ - end_rev = SVN_IS_VALID_REVNUM(end_rev) ? end_rev : 0; + if (! SVN_IS_VALID_REVNUM(end_rev)) + end_rev = 0; + else if (end_rev > start_rev) + return svn_error_createf(SVN_ERR_FS_NO_SUCH_REVISION, NULL, + _("No such revision %ld"), end_rev); /* Are the revision properly ordered? They better be -- the API demands it. */ @@ -1006,26 +1012,39 @@ get_merged_mergeinfo(apr_hash_t **merged_mergeinfo, apr_hash_t *curr_mergeinfo, *prev_mergeinfo, *deleted, *changed; svn_error_t *err; svn_fs_root_t *root, *prev_root; - apr_hash_t *changed_paths; - const char *path = old_path_rev->path; + const char *start_path = old_path_rev->path; + const char *path = NULL; + + svn_fs_path_change_iterator_t *iterator; + svn_fs_path_change3_t *change; /* Getting/parsing/diffing svn:mergeinfo is expensive, so only do it if there is a property change. */ SVN_ERR(svn_fs_revision_root(&root, repos->fs, old_path_rev->revnum, scratch_pool)); - SVN_ERR(svn_fs_paths_changed2(&changed_paths, root, scratch_pool)); - while (1) + SVN_ERR(svn_fs_paths_changed3(&iterator, root, scratch_pool, scratch_pool)); + SVN_ERR(svn_fs_path_change_get(&change, iterator)); + + /* Find the changed PATH closest to START_PATH which may have a mergeinfo + * change. */ + while (change) { - svn_fs_path_change2_t *changed_path = svn_hash_gets(changed_paths, path); - if (changed_path && changed_path->prop_mod - && changed_path->mergeinfo_mod != svn_tristate_false) - break; - if (svn_fspath__is_root(path, strlen(path))) + if ( change->prop_mod + && change->mergeinfo_mod != svn_tristate_false + && svn_fspath__skip_ancestor(change->path.data, start_path)) { - *merged_mergeinfo = NULL; - return SVN_NO_ERROR; + if (!path || svn_fspath__skip_ancestor(path, change->path.data)) + path = apr_pstrmemdup(scratch_pool, change->path.data, + change->path.len); } - path = svn_fspath__dirname(path, scratch_pool); + + SVN_ERR(svn_fs_path_change_get(&change, iterator)); + } + + if (path == NULL) + { + *merged_mergeinfo = NULL; + return SVN_NO_ERROR; } /* First, find the mergeinfo difference for old_path_rev->revnum, and @@ -1350,22 +1369,48 @@ send_path_revision(struct path_revision *path_rev, void *delta_baton = NULL; apr_pool_t *tmp_pool; /* For swapping */ svn_boolean_t contents_changed; + svn_boolean_t props_changed; svn_pool_clear(sb->iterpool); /* Get the revision properties. */ - SVN_ERR(svn_fs_revision_proplist(&rev_props, repos->fs, - path_rev->revnum, sb->iterpool)); + SVN_ERR(svn_fs_revision_proplist2(&rev_props, repos->fs, + path_rev->revnum, FALSE, + sb->iterpool, sb->iterpool)); /* Open the revision root. */ SVN_ERR(svn_fs_revision_root(&root, repos->fs, path_rev->revnum, sb->iterpool)); - /* Get the file's properties for this revision and compute the diffs. */ - SVN_ERR(svn_fs_node_proplist(&props, root, path_rev->path, + /* Check if the props *may* have changed. */ + if (sb->last_root) + { + /* We don't use svn_fs_props_different() because it's more + * expensive. */ + SVN_ERR(svn_fs_props_changed(&props_changed, + sb->last_root, sb->last_path, + root, path_rev->path, sb->iterpool)); + } + else + { + props_changed = TRUE; + } + + /* Calculate actual difference between last and current properties. */ + if (props_changed) + { + /* Get the file's properties for this revision and compute the diffs. */ + SVN_ERR(svn_fs_node_proplist(&props, root, path_rev->path, sb->iterpool)); - SVN_ERR(svn_prop_diffs(&prop_diffs, props, sb->last_props, - sb->iterpool)); + SVN_ERR(svn_prop_diffs(&prop_diffs, props, sb->last_props, + sb->iterpool)); + } + else + { + /* Properties didn't change: copy LAST_PROPS to current POOL. */ + props = svn_prop_hash_dup(sb->last_props, sb->iterpool); + prop_diffs = apr_array_make(sb->iterpool, 0, sizeof(svn_prop_t)); + } /* Check if the contents *may* have changed. */ if (! sb->last_root) @@ -1590,6 +1635,10 @@ svn_repos_get_file_revs2(svn_repos_t *repos, end = youngest_rev; } + /* Make sure we catch up on the latest revprop changes. This is the only + * time we will refresh the revprop data in this query. */ + SVN_ERR(svn_fs_refresh_revision_props(repos->fs, scratch_pool)); + if (end < start) { if (include_merged_revisions) |