diff options
author | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
---|---|---|
committer | peter <peter@FreeBSD.org> | 2013-06-18 02:07:41 +0000 |
commit | d25dac7fcc6acc838b71bbda8916fd9665c709ab (patch) | |
tree | 135691142dc0e75a5e5d97b5074d03436435b8e0 /subversion/libsvn_ra_serf | |
download | FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.zip FreeBSD-src-d25dac7fcc6acc838b71bbda8916fd9665c709ab.tar.gz |
Import trimmed svn-1.8.0-rc3
Diffstat (limited to 'subversion/libsvn_ra_serf')
25 files changed, 19699 insertions, 0 deletions
diff --git a/subversion/libsvn_ra_serf/README b/subversion/libsvn_ra_serf/README new file mode 100644 index 0000000..d3baf33 --- /dev/null +++ b/subversion/libsvn_ra_serf/README @@ -0,0 +1,84 @@ +ra_serf status +============== + +This library is an RA-layer implementation of a WebDAV client that uses Serf. + +Serf's homepage is at: + http://code.google.com/p/serf/ + +The latest serf releases can be fetched at: + http://code.google.com/p/serf/downloads/list + +The latest serf sources can be fetched via SVN at: + http://serf.googlecode.com/svn/trunk/ + +ra_serf can be enabled with the following configure flags: + "--with-serf=/path/to/serf/install" +As Neon is currently Subversion's default RA DAV layer, you also need +to add "http-library = serf" to your ~/.subversion/servers file to +choose ra_serf at runtime. Alternately, you can build with only +support for ra_serf: + "--without-neon --with-serf=/path/to/serf/install" + +For more about how ra_serf/ra_neon talk WebDAV, consult notes/webdav-protocol. + +Working copies are interchangable between ra_serf and ra_neon. (They both use +the svn:wc:ra_dav:version-url property to store the latest revision of a file.) + +Completed tasks +--------------- +- Core functionality complete (see regression test status below) +- https support (SSL) +- Basic authentication +- Update parallelization/pipelining (also for status/diff/switch/etc) + - Does not require inline base64-encoding of content + - 4 connections are open on an update (matches browser's default behavior) + - 1 connection is used for the REPORT; 3 are used to fetch files & props +- Supports http-compression config flag +- SSL client and server certificates +- Proxy support +- NTLM/SSPI integration for Windows folks +- REPORT body buckets can now be read twice (#3212) + +Regression test status +---------------------- +All current regression tests are known to pass on: + - Debian/AMD64 with APR 1.3.x + - Mac OS X + - Solaris + - Windows + +Things to do before the next release (1.6.x timeframe) +------------------------------------------------------ + +- Digest authentication + +- Fix the editor API violation (TBC, #2932) + +Nice to haves +------------- + +- Move some of the code from ra_serf into serf. Serf doesn't have a very + high-level API; but the code in util.c can go a long way towards that. + +- Commit parallellization/pipelining + - Determine how to use HTTP pipelining and multiple connections for commit + - May need response from CHECKOUT to issue PUT/PROPPATCH + - ra_svn has a custom commit pipelining that may be worth investigating too + +- Use PROPFIND Depth: 1 when we are adding a directory locally to skip + fetching properties on files + +- Discover server's keep-alive setting via OPTIONS requests and notify serf + +- Fix bug in mod_dav_svn that omits remove-prop in the update-report when a + lock is broken and send-all is false. + (See upd_change_xxx_prop in mod_dav_svn/update.c) + +- Fix bug in mod_dav_svn/mod_deflate that causes it to hold onto the entire + REPORT response until it is completed. (This is why ra_serf doesn't request + gzip compression on the REPORT requests.) + +- Remove remaining abort()s - ;-) aka add better debug logging + +- Support for HTTP/1.0 pnly proxies. diff --git a/subversion/libsvn_ra_serf/blame.c b/subversion/libsvn_ra_serf/blame.c new file mode 100644 index 0000000..fa4243c --- /dev/null +++ b/subversion/libsvn_ra_serf/blame.c @@ -0,0 +1,375 @@ +/* + * blame.c : entry point for blame RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_path.h" +#include "svn_base64.h" +#include "svn_props.h" + +#include "svn_private_config.h" + +#include "private/svn_string_private.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +typedef enum blame_state_e { + INITIAL = 0, + FILE_REVS_REPORT, + FILE_REV, + REV_PROP, + SET_PROP, + REMOVE_PROP, + MERGED_REVISION, + TXDELTA +} blame_state_e; + + +typedef struct blame_context_t { + /* pool passed to get_file_revs */ + apr_pool_t *pool; + + /* parameters set by our caller */ + const char *path; + svn_revnum_t start; + svn_revnum_t end; + svn_boolean_t include_merged_revisions; + + /* blame handler and baton */ + svn_file_rev_handler_t file_rev; + void *file_rev_baton; + + /* As we parse each FILE_REV, we collect data in these variables: + property changes and new content. STREAM is valid when we're + in the TXDELTA state, processing the incoming cdata. */ + apr_hash_t *rev_props; + apr_array_header_t *prop_diffs; + apr_pool_t *state_pool; /* put property stuff in here */ + + svn_stream_t *stream; + +} blame_context_t; + + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t blame_ttable[] = { + { INITIAL, S_, "file-revs-report", FILE_REVS_REPORT, + FALSE, { NULL }, FALSE }, + + { FILE_REVS_REPORT, S_, "file-rev", FILE_REV, + FALSE, { "path", "rev", NULL }, TRUE }, + + { FILE_REV, S_, "rev-prop", REV_PROP, + TRUE, { "name", "?encoding", NULL }, TRUE }, + + { FILE_REV, S_, "set-prop", SET_PROP, + TRUE, { "name", "?encoding", NULL }, TRUE }, + + { FILE_REV, S_, "remove-prop", REMOVE_PROP, + FALSE, { "name", NULL }, TRUE }, + + { FILE_REV, S_, "merged-revision", MERGED_REVISION, + FALSE, { NULL }, TRUE }, + + { FILE_REV, S_, "txdelta", TXDELTA, + FALSE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_opened_t */ +static svn_error_t * +blame_opened(svn_ra_serf__xml_estate_t *xes, + void *baton, + int entered_state, + const svn_ra_serf__dav_props_t *tag, + apr_pool_t *scratch_pool) +{ + blame_context_t *blame_ctx = baton; + + if (entered_state == FILE_REV) + { + apr_pool_t *state_pool = svn_ra_serf__xml_state_pool(xes); + + /* Child elements will store properties in these structures. */ + blame_ctx->rev_props = apr_hash_make(state_pool); + blame_ctx->prop_diffs = apr_array_make(state_pool, + 5, sizeof(svn_prop_t)); + blame_ctx->state_pool = state_pool; + + /* Clear this, so we can detect the absence of a TXDELTA. */ + blame_ctx->stream = NULL; + } + else if (entered_state == TXDELTA) + { + apr_pool_t *state_pool = svn_ra_serf__xml_state_pool(xes); + apr_hash_t *gathered = svn_ra_serf__xml_gather_since(xes, FILE_REV); + const char *path; + const char *rev; + const char *merged_revision; + svn_txdelta_window_handler_t txdelta; + void *txdelta_baton; + + path = svn_hash_gets(gathered, "path"); + rev = svn_hash_gets(gathered, "rev"); + merged_revision = svn_hash_gets(gathered, "merged-revision"); + + SVN_ERR(blame_ctx->file_rev(blame_ctx->file_rev_baton, + path, SVN_STR_TO_REV(rev), + blame_ctx->rev_props, + merged_revision != NULL, + &txdelta, &txdelta_baton, + blame_ctx->prop_diffs, + state_pool)); + + blame_ctx->stream = svn_base64_decode(svn_txdelta_parse_svndiff( + txdelta, txdelta_baton, + TRUE /* error_on_early_close */, + state_pool), + state_pool); + } + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +blame_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + blame_context_t *blame_ctx = baton; + + if (leaving_state == FILE_REV) + { + /* Note that we test STREAM, but any pointer is currently invalid. + It was closed when left the TXDELTA state. */ + if (blame_ctx->stream == NULL) + { + const char *path; + const char *rev; + + path = svn_hash_gets(attrs, "path"); + rev = svn_hash_gets(attrs, "rev"); + + /* Send a "no content" notification. */ + SVN_ERR(blame_ctx->file_rev(blame_ctx->file_rev_baton, + path, SVN_STR_TO_REV(rev), + blame_ctx->rev_props, + FALSE /* result_of_merge */, + NULL, NULL, /* txdelta / baton */ + blame_ctx->prop_diffs, + scratch_pool)); + } + } + else if (leaving_state == MERGED_REVISION) + { + svn_ra_serf__xml_note(xes, FILE_REV, "merged-revision", "*"); + } + else if (leaving_state == TXDELTA) + { + SVN_ERR(svn_stream_close(blame_ctx->stream)); + } + else + { + const char *name; + const svn_string_t *value; + + SVN_ERR_ASSERT(leaving_state == REV_PROP + || leaving_state == SET_PROP + || leaving_state == REMOVE_PROP); + + name = apr_pstrdup(blame_ctx->state_pool, + svn_hash_gets(attrs, "name")); + + if (leaving_state == REMOVE_PROP) + { + value = NULL; + } + else + { + const char *encoding = svn_hash_gets(attrs, "encoding"); + + if (encoding && strcmp(encoding, "base64") == 0) + value = svn_base64_decode_string(cdata, blame_ctx->state_pool); + else + value = svn_string_dup(cdata, blame_ctx->state_pool); + } + + if (leaving_state == REV_PROP) + { + svn_hash_sets(blame_ctx->rev_props, name, value); + } + else + { + svn_prop_t *prop = apr_array_push(blame_ctx->prop_diffs); + + prop->name = name; + prop->value = value; + } + } + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_cdata_t */ +static svn_error_t * +blame_cdata(svn_ra_serf__xml_estate_t *xes, + void *baton, + int current_state, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + blame_context_t *blame_ctx = baton; + + if (current_state == TXDELTA) + { + SVN_ERR(svn_stream_write(blame_ctx->stream, data, &len)); + /* Ignore the returned LEN value. */ + } + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_file_revs_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + blame_context_t *blame_ctx = baton; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, + "S:file-revs-report", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "S:start-revision", apr_ltoa(pool, blame_ctx->start), + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:end-revision", apr_ltoa(pool, blame_ctx->end), + alloc); + + if (blame_ctx->include_merged_revisions) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:include-merged-revisions", NULL, + alloc); + } + + svn_ra_serf__add_tag_buckets(buckets, + "S:path", blame_ctx->path, + alloc); + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, + "S:file-revs-report"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_file_revs(svn_ra_session_t *ra_session, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t include_merged_revisions, + svn_file_rev_handler_t rev_handler, + void *rev_handler_baton, + apr_pool_t *pool) +{ + blame_context_t *blame_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url; + svn_error_t *err; + + blame_ctx = apr_pcalloc(pool, sizeof(*blame_ctx)); + blame_ctx->pool = pool; + blame_ctx->path = path; + blame_ctx->file_rev = rev_handler; + blame_ctx->file_rev_baton = rev_handler_baton; + blame_ctx->start = start; + blame_ctx->end = end; + blame_ctx->include_merged_revisions = include_merged_revisions; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, end, + pool, pool)); + + xmlctx = svn_ra_serf__xml_context_create(blame_ttable, + blame_opened, + blame_closed, + blame_cdata, + blame_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_type = "text/xml"; + handler->body_delegate = create_file_revs_body; + handler->body_delegate_baton = blame_ctx; + handler->conn = session->conns[0]; + handler->session = session; + + err = svn_ra_serf__context_run_one(handler, pool); + + err = svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location), + err); + + return svn_error_trace(err); +} diff --git a/subversion/libsvn_ra_serf/blncache.c b/subversion/libsvn_ra_serf/blncache.c new file mode 100644 index 0000000..d6abcdf --- /dev/null +++ b/subversion/libsvn_ra_serf/blncache.c @@ -0,0 +1,179 @@ +/* + * blncache.c: DAV baseline information cache. + * + * ==================================================================== + * 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 "svn_hash.h" +#include "svn_dirent_uri.h" +#include "svn_types.h" +#include "svn_pools.h" + +#include "blncache.h" + +/* Baseline information cache object. */ +typedef struct baseline_info_t +{ + const char *bc_url; /* baseline collection URL. */ + svn_revnum_t revision; /* revision associated with the baseline. */ + +} baseline_info_t; + +/* Module-private structure used to hold the caches. */ +struct svn_ra_serf__blncache_t +{ + /* A hash mapping 'svn_revnum_t *' baseline revisions to 'const + * char *' baseline collection URLs. + */ + apr_hash_t *revnum_to_bc; + + /* A hash mapping 'const char *' baseline URLs to 'baseline_info_t *' + * structures. (Allocated from the same pool as 'revnum_to_bc'.) + */ + apr_hash_t *baseline_info; +}; + + + +/* Return a pointer to an 'baseline_info_t' structure allocated from + * POOL and populated with BC_URL (which is duped into POOL) and + * REVISION. + */ +static baseline_info_t * +baseline_info_make(const char *bc_url, + svn_revnum_t revision, + apr_pool_t *pool) +{ + baseline_info_t *result = apr_palloc(pool, sizeof(*result)); + + result->bc_url = apr_pstrdup(pool, bc_url); + result->revision = revision; + + return result; +} + +/* Set in HASH the value VAL for the KEY (whose key length is KLEN). + * KEY will be duped into HASH's pool. + */ +static void +hash_set_copy(apr_hash_t *hash, + const void *key, + apr_ssize_t klen, + const void *val) +{ + if (klen == APR_HASH_KEY_STRING) + klen = strlen(key); + apr_hash_set(hash, apr_pmemdup(apr_hash_pool_get(hash), key, klen), + klen, val); +} + + +svn_error_t * +svn_ra_serf__blncache_create(svn_ra_serf__blncache_t **blncache_p, + apr_pool_t *pool) +{ + svn_ra_serf__blncache_t *blncache = apr_pcalloc(pool, sizeof(*blncache)); + apr_pool_t *cache_pool; + + /* Create subpool for cached data. It will be cleared if we reach maximum + * cache size.*/ + cache_pool = svn_pool_create(pool); + blncache->revnum_to_bc = apr_hash_make(cache_pool); + blncache->baseline_info = apr_hash_make(cache_pool); + + *blncache_p = blncache; + + return SVN_NO_ERROR; +} + +#define MAX_CACHE_SIZE 1000 + +svn_error_t * +svn_ra_serf__blncache_set(svn_ra_serf__blncache_t *blncache, + const char *baseline_url, + svn_revnum_t revision, + const char *bc_url, + apr_pool_t *pool) +{ + if (bc_url && SVN_IS_VALID_REVNUM(revision)) + { + apr_pool_t *cache_pool = apr_hash_pool_get(blncache->revnum_to_bc); + + /* If the caches are too big, delete and recreate 'em and move along. */ + if (MAX_CACHE_SIZE < (apr_hash_count(blncache->baseline_info) + + apr_hash_count(blncache->revnum_to_bc))) + { + svn_pool_clear(cache_pool); + blncache->revnum_to_bc = apr_hash_make(cache_pool); + blncache->baseline_info = apr_hash_make(cache_pool); + } + + hash_set_copy(blncache->revnum_to_bc, &revision, sizeof(revision), + apr_pstrdup(cache_pool, bc_url)); + + if (baseline_url) + { + hash_set_copy(blncache->baseline_info, baseline_url, + APR_HASH_KEY_STRING, + baseline_info_make(bc_url, revision, cache_pool)); + } + } + + return SVN_NO_ERROR; +} + +#undef MAX_CACHE_SIZE + +svn_error_t * +svn_ra_serf__blncache_get_bc_url(const char **bc_url_p, + svn_ra_serf__blncache_t *blncache, + svn_revnum_t revnum, + apr_pool_t *pool) +{ + const char *value = apr_hash_get(blncache->revnum_to_bc, + &revnum, sizeof(revnum)); + *bc_url_p = value ? apr_pstrdup(pool, value) : NULL; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__blncache_get_baseline_info(const char **bc_url_p, + svn_revnum_t *revision_p, + svn_ra_serf__blncache_t *blncache, + const char *baseline_url, + apr_pool_t *pool) +{ + baseline_info_t *info = svn_hash_gets(blncache->baseline_info, baseline_url); + if (info) + { + *bc_url_p = apr_pstrdup(pool, info->bc_url); + *revision_p = info->revision; + } + else + { + *bc_url_p = NULL; + *revision_p = SVN_INVALID_REVNUM; + } + + return SVN_NO_ERROR; +} + diff --git a/subversion/libsvn_ra_serf/blncache.h b/subversion/libsvn_ra_serf/blncache.h new file mode 100644 index 0000000..5ad4eba --- /dev/null +++ b/subversion/libsvn_ra_serf/blncache.h @@ -0,0 +1,90 @@ +/* + * blncache.h: DAV baseline information cache. + * + * ==================================================================== + * 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_LIBSVN_RA_SERF_BLNCACHE_H +#define SVN_LIBSVN_RA_SERF_BLNCACHE_H + +#include <apr_pools.h> + +#include "svn_types.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/* Baseline information cache. Baseline information cache holds information + * about DAV baseline (bln): + * 1. URL of the baseline (bln) + * 2. Revision number associated with baseline + * 3. URL of baseline collection (bc). + */ +typedef struct svn_ra_serf__blncache_t svn_ra_serf__blncache_t; + +/* Creates new instance of baseline cache. Sets BLNCACHE_P with + * a pointer to new instance, allocated in POOL. + */ +svn_error_t * +svn_ra_serf__blncache_create(svn_ra_serf__blncache_t **blncache_p, + apr_pool_t *pool); + +/* Add information about baseline. BLNCACHE is a pointer to + * baseline cache previously created using svn_ra_serf__blncache_create + * function. BASELINE_URL is URL of baseline (can be NULL if unknown). + * REVNUM is revision number associated with baseline. Use SVN_INVALID_REVNUM + * to indicate that revision is unknown. + * BC_URL is URL of baseline collection (can be NULL if unknwon). + */ +svn_error_t * +svn_ra_serf__blncache_set(svn_ra_serf__blncache_t *blncache, + const char *baseline_url, + svn_revnum_t revnum, + const char *bc_url, + apr_pool_t *pool); + +/* Sets *BC_URL_P with a pointer to baseline collection URL for the given + * REVNUM. *BC_URL_P will be NULL if cache doesn't have information about + * this baseline. + */ +svn_error_t * +svn_ra_serf__blncache_get_bc_url(const char **bc_url_p, + svn_ra_serf__blncache_t *blncache, + svn_revnum_t revnum, + apr_pool_t *pool); + +/* Sets *BC_URL_P with pointer to baseline collection URL and *REVISION_P + * with revision number of baseline BASELINE_URL. *BC_URL_P will be NULL, + * *REVNUM_P will SVN_INVALID_REVNUM if cache doesn't have such + * information. + */ +svn_error_t * +svn_ra_serf__blncache_get_baseline_info(const char **bc_url_p, + svn_revnum_t *revnum_p, + svn_ra_serf__blncache_t *blncache, + const char *baseline_url, + apr_pool_t *pool); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVN_LIBSVN_RA_SERF_BLNCACHE_H*/ diff --git a/subversion/libsvn_ra_serf/commit.c b/subversion/libsvn_ra_serf/commit.c new file mode 100644 index 0000000..aaf1717 --- /dev/null +++ b/subversion/libsvn_ra_serf/commit.c @@ -0,0 +1,2468 @@ +/* + * commit.c : entry point for commit RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_base64.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_props.h" + +#include "svn_private_config.h" +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_skel.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* Baton passed back with the commit editor. */ +typedef struct commit_context_t { + /* Pool for our commit. */ + apr_pool_t *pool; + + svn_ra_serf__session_t *session; + svn_ra_serf__connection_t *conn; + + apr_hash_t *revprop_table; + + svn_commit_callback2_t callback; + void *callback_baton; + + apr_hash_t *lock_tokens; + svn_boolean_t keep_locks; + apr_hash_t *deleted_entries; /* deleted files (for delete+add detection) */ + + /* HTTP v2 stuff */ + const char *txn_url; /* txn URL (!svn/txn/TXN_NAME) */ + const char *txn_root_url; /* commit anchor txn root URL */ + + /* HTTP v1 stuff (only valid when 'txn_url' is NULL) */ + const char *activity_url; /* activity base URL... */ + const char *baseline_url; /* the working-baseline resource */ + const char *checked_in_url; /* checked-in root to base CHECKOUTs from */ + const char *vcc_url; /* vcc url */ + +} commit_context_t; + +#define USING_HTTPV2_COMMIT_SUPPORT(commit_ctx) ((commit_ctx)->txn_url != NULL) + +/* Structure associated with a PROPPATCH request. */ +typedef struct proppatch_context_t { + apr_pool_t *pool; + + const char *relpath; + const char *path; + + commit_context_t *commit; + + /* Changed and removed properties. */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* Same, for the old value (*old_value_p). */ + apr_hash_t *previous_changed_props; + apr_hash_t *previous_removed_props; + + /* In HTTP v2, this is the file/directory version we think we're changing. */ + svn_revnum_t base_revision; + +} proppatch_context_t; + +typedef struct delete_context_t { + const char *path; + + svn_revnum_t revision; + + const char *lock_token; + apr_hash_t *lock_token_hash; + svn_boolean_t keep_locks; + +} delete_context_t; + +/* Represents a directory. */ +typedef struct dir_context_t { + /* Pool for our directory. */ + apr_pool_t *pool; + + /* The root commit we're in progress for. */ + commit_context_t *commit; + + /* URL to operate against (used for CHECKOUT and PROPPATCH before + HTTP v2, for PROPPATCH in HTTP v2). */ + const char *url; + + /* How many pending changes we have left in this directory. */ + unsigned int ref_count; + + /* Is this directory being added? (Otherwise, just opened.) */ + svn_boolean_t added; + + /* Our parent */ + struct dir_context_t *parent_dir; + + /* The directory name; if "", we're the 'root' */ + const char *relpath; + + /* The basename of the directory. "" for the 'root' */ + const char *name; + + /* The base revision of the dir. */ + svn_revnum_t base_revision; + + const char *copy_path; + svn_revnum_t copy_revision; + + /* Changed and removed properties */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* The checked-out working resource for this directory. May be NULL; if so + call checkout_dir() first. */ + const char *working_url; + +} dir_context_t; + +/* Represents a file to be committed. */ +typedef struct file_context_t { + /* Pool for our file. */ + apr_pool_t *pool; + + /* The root commit we're in progress for. */ + commit_context_t *commit; + + /* Is this file being added? (Otherwise, just opened.) */ + svn_boolean_t added; + + dir_context_t *parent_dir; + + const char *relpath; + const char *name; + + /* The checked-out working resource for this file. */ + const char *working_url; + + /* The base revision of the file. */ + svn_revnum_t base_revision; + + /* Copy path and revision */ + const char *copy_path; + svn_revnum_t copy_revision; + + /* stream */ + svn_stream_t *stream; + + /* Temporary file containing the svndiff. */ + apr_file_t *svndiff; + + /* Our base checksum as reported by the WC. */ + const char *base_checksum; + + /* Our resulting checksum as reported by the WC. */ + const char *result_checksum; + + /* Changed and removed properties. */ + apr_hash_t *changed_props; + apr_hash_t *removed_props; + + /* URL to PUT the file at. */ + const char *url; + +} file_context_t; + + +/* Setup routines and handlers for various requests we'll invoke. */ + +static svn_error_t * +return_response_err(svn_ra_serf__handler_t *handler) +{ + svn_error_t *err; + + /* We should have captured SLINE and LOCATION in the HANDLER. */ + SVN_ERR_ASSERT(handler->handler_pool != NULL); + + /* Ye Olde Fallback Error */ + err = svn_error_compose_create( + handler->server_error != NULL + ? handler->server_error->error + : SVN_NO_ERROR, + svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("%s of '%s': %d %s"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason)); + + /* Try to return one of the standard errors for 301, 404, etc., + then look for an error embedded in the response. */ + return svn_error_compose_create(svn_ra_serf__error_on_status( + handler->sline.code, + handler->path, + handler->location), + err); +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_checkout_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + const char *activity_url = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body_bkt, alloc); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:checkout", + "xmlns:D", "DAV:", + NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:activity-set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:href", NULL); + + SVN_ERR_ASSERT(activity_url != NULL); + svn_ra_serf__add_cdata_len_buckets(body_bkt, alloc, + activity_url, + strlen(activity_url)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:href"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:activity-set"); + svn_ra_serf__add_tag_buckets(body_bkt, "D:apply-to-version", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:checkout"); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + + +/* Using the HTTPv1 protocol, perform a CHECKOUT of NODE_URL within the + given COMMIT_CTX. The resulting working resource will be returned in + *WORKING_URL, allocated from RESULT_POOL. All temporary allocations + are performed in SCRATCH_POOL. + + ### are these URLs actually repos relpath values? or fspath? or maybe + ### the abspath portion of the full URL. + + This function operates synchronously. + + Strictly speaking, we could perform "all" of the CHECKOUT requests + when the commit starts, and only block when we need a specific + answer. Or, at a minimum, send off these individual requests async + and block when we need the answer (eg PUT or PROPPATCH). + + However: the investment to speed this up is not worthwhile, given + that CHECKOUT (and the related round trip) is completely obviated + in HTTPv2. +*/ +static svn_error_t * +checkout_node(const char **working_url, + const commit_context_t *commit_ctx, + const char *node_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t handler = { 0 }; + apr_status_t status; + apr_uri_t uri; + + /* HANDLER_POOL is the scratch pool since we don't need to remember + anything from the handler. We just want the working resource. */ + handler.handler_pool = scratch_pool; + handler.session = commit_ctx->session; + handler.conn = commit_ctx->conn; + + handler.body_delegate = create_checkout_body; + handler.body_delegate_baton = (/* const */ void *)commit_ctx->activity_url; + handler.body_type = "text/xml"; + + handler.response_handler = svn_ra_serf__expect_empty_body; + handler.response_baton = &handler; + + handler.method = "CHECKOUT"; + handler.path = node_url; + + SVN_ERR(svn_ra_serf__context_run_one(&handler, scratch_pool)); + + if (handler.sline.code != 201) + return svn_error_trace(return_response_err(&handler)); + + if (handler.location == NULL) + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("No Location header received")); + + /* We only want the path portion of the Location header. + (code.google.com sometimes returns an 'http:' scheme for an + 'https:' transaction ... we'll work around that by stripping the + scheme, host, and port here and re-adding the correct ones + later. */ + status = apr_uri_parse(scratch_pool, handler.location, &uri); + if (status) + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Error parsing Location header value")); + + *working_url = svn_urlpath__canonicalize(uri.path, result_pool); + + return SVN_NO_ERROR; +} + + +/* This is a wrapper around checkout_node() (which see for + documentation) which simply retries the CHECKOUT request when it + fails due to an SVN_ERR_APMOD_BAD_BASELINE error return from the + server. + + See http://subversion.tigris.org/issues/show_bug.cgi?id=4127 for + details. +*/ +static svn_error_t * +retry_checkout_node(const char **working_url, + const commit_context_t *commit_ctx, + const char *node_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_error_t *err = SVN_NO_ERROR; + int retry_count = 5; /* Magic, arbitrary number. */ + + do + { + svn_error_clear(err); + + err = checkout_node(working_url, commit_ctx, node_url, + result_pool, scratch_pool); + + /* There's a small chance of a race condition here if Apache is + experiencing heavy commit concurrency or if the network has + long latency. It's possible that the value of HEAD changed + between the time we fetched the latest baseline and the time + we try to CHECKOUT that baseline. If that happens, Apache + will throw us a BAD_BASELINE error (deltaV says you can only + checkout the latest baseline). We just ignore that specific + error and retry a few times, asking for the latest baseline + again. */ + if (err && (err->apr_err != SVN_ERR_APMOD_BAD_BASELINE)) + return err; + } + while (err && retry_count--); + + return err; +} + + +static svn_error_t * +checkout_dir(dir_context_t *dir, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + dir_context_t *p_dir = dir; + const char *checkout_url; + const char **working; + + if (dir->working_url) + { + return SVN_NO_ERROR; + } + + /* Is this directory or one of our parent dirs newly added? + * If so, we're already implicitly checked out. */ + while (p_dir) + { + if (p_dir->added) + { + /* Implicitly checkout this dir now. */ + dir->working_url = svn_path_url_add_component2( + dir->parent_dir->working_url, + dir->name, dir->pool); + return SVN_NO_ERROR; + } + p_dir = p_dir->parent_dir; + } + + /* We could be called twice for the root: once to checkout the baseline; + * once to checkout the directory itself if we need to do so. + * Note: CHECKOUT_URL should live longer than HANDLER. + */ + if (!dir->parent_dir && !dir->commit->baseline_url) + { + checkout_url = dir->commit->vcc_url; + working = &dir->commit->baseline_url; + } + else + { + checkout_url = dir->url; + working = &dir->working_url; + } + + /* Checkout our directory into the activity URL now. */ + err = retry_checkout_node(working, dir->commit, checkout_url, + dir->pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_CONFLICT) + SVN_ERR_W(err, apr_psprintf(scratch_pool, + _("Directory '%s' is out of date; try updating"), + svn_dirent_local_style(dir->relpath, scratch_pool))); + return err; + } + + return SVN_NO_ERROR; +} + + +/* Set *CHECKED_IN_URL to the appropriate DAV version url for + * RELPATH (relative to the root of SESSION). + * + * Try to find this version url in three ways: + * First, if SESSION->callbacks->get_wc_prop() is defined, try to read the + * version url from the working copy properties. + * Second, if the version url of the parent directory PARENT_VSN_URL is + * defined, set *CHECKED_IN_URL to the concatenation of PARENT_VSN_URL with + * RELPATH. + * Else, fetch the version url for the root of SESSION using CONN and + * BASE_REVISION, and set *CHECKED_IN_URL to the concatenation of that + * with RELPATH. + * + * Allocate the result in RESULT_POOL, and use SCRATCH_POOL for + * temporary allocation. + */ +static svn_error_t * +get_version_url(const char **checked_in_url, + svn_ra_serf__session_t *session, + const char *relpath, + svn_revnum_t base_revision, + const char *parent_vsn_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *root_checkout; + + if (session->wc_callbacks->get_wc_prop) + { + const svn_string_t *current_version; + + SVN_ERR(session->wc_callbacks->get_wc_prop( + session->wc_callback_baton, + relpath, + SVN_RA_SERF__WC_CHECKED_IN_URL, + ¤t_version, scratch_pool)); + + if (current_version) + { + *checked_in_url = + svn_urlpath__canonicalize(current_version->data, result_pool); + return SVN_NO_ERROR; + } + } + + if (parent_vsn_url) + { + root_checkout = parent_vsn_url; + } + else + { + const char *propfind_url; + svn_ra_serf__connection_t *conn = session->conns[0]; + + if (SVN_IS_VALID_REVNUM(base_revision)) + { + /* mod_dav_svn can't handle the "Label:" header that + svn_ra_serf__deliver_props() is going to try to use for + this lookup, so we'll do things the hard(er) way, by + looking up the version URL from a resource in the + baseline collection. */ + /* ### conn==NULL for session->conns[0]. same as CONN. */ + SVN_ERR(svn_ra_serf__get_stable_url(&propfind_url, + NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, base_revision, + scratch_pool, scratch_pool)); + } + else + { + propfind_url = session->session_url.path; + } + + SVN_ERR(svn_ra_serf__fetch_dav_prop(&root_checkout, + conn, propfind_url, base_revision, + "checked-in", + scratch_pool, scratch_pool)); + if (!root_checkout) + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("Path '%s' not present"), + session->session_url.path); + + root_checkout = svn_urlpath__canonicalize(root_checkout, scratch_pool); + } + + *checked_in_url = svn_path_url_add_component2(root_checkout, relpath, + result_pool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +checkout_file(file_context_t *file, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + dir_context_t *parent_dir = file->parent_dir; + const char *checkout_url; + + /* Is one of our parent dirs newly added? If so, we're already + * implicitly checked out. + */ + while (parent_dir) + { + if (parent_dir->added) + { + /* Implicitly checkout this file now. */ + file->working_url = svn_path_url_add_component2( + parent_dir->working_url, + svn_relpath_skip_ancestor( + parent_dir->relpath, file->relpath), + file->pool); + return SVN_NO_ERROR; + } + parent_dir = parent_dir->parent_dir; + } + + SVN_ERR(get_version_url(&checkout_url, + file->commit->session, + file->relpath, file->base_revision, + NULL, scratch_pool, scratch_pool)); + + /* Checkout our file into the activity URL now. */ + err = retry_checkout_node(&file->working_url, file->commit, checkout_url, + file->pool, scratch_pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_CONFLICT) + SVN_ERR_W(err, apr_psprintf(scratch_pool, + _("File '%s' is out of date; try updating"), + svn_dirent_local_style(file->relpath, scratch_pool))); + return err; + } + + return SVN_NO_ERROR; +} + +/* Helper function for proppatch_walker() below. */ +static svn_error_t * +get_encoding_and_cdata(const char **encoding_p, + const svn_string_t **encoded_value_p, + serf_bucket_alloc_t *alloc, + const svn_string_t *value, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + if (value == NULL) + { + *encoding_p = NULL; + *encoded_value_p = NULL; + return SVN_NO_ERROR; + } + + /* If a property is XML-safe, XML-encode it. Else, base64-encode + it. */ + if (svn_xml_is_xml_safe(value->data, value->len)) + { + svn_stringbuf_t *xml_esc = NULL; + svn_xml_escape_cdata_string(&xml_esc, value, scratch_pool); + *encoding_p = NULL; + *encoded_value_p = svn_string_create_from_buf(xml_esc, result_pool); + } + else + { + *encoding_p = "base64"; + *encoded_value_p = svn_base64_encode_string2(value, TRUE, result_pool); + } + + return SVN_NO_ERROR; +} + +typedef struct walker_baton_t { + serf_bucket_t *body_bkt; + apr_pool_t *body_pool; + + apr_hash_t *previous_changed_props; + apr_hash_t *previous_removed_props; + + const char *path; + + /* Hack, since change_rev_prop(old_value_p != NULL, value = NULL) uses D:set + rather than D:remove... (see notes/http-and-webdav/webdav-protocol) */ + enum { + filter_all_props, + filter_props_with_old_value, + filter_props_without_old_value + } filter; + + /* Is the property being deleted? */ + svn_boolean_t deleting; +} walker_baton_t; + +/* If we have (recorded in WB) the old value of the property named NS:NAME, + * then set *HAVE_OLD_VAL to TRUE and set *OLD_VAL_P to that old value + * (which may be NULL); else set *HAVE_OLD_VAL to FALSE. */ +static svn_error_t * +derive_old_val(svn_boolean_t *have_old_val, + const svn_string_t **old_val_p, + walker_baton_t *wb, + const char *ns, + const char *name) +{ + *have_old_val = FALSE; + + if (wb->previous_changed_props) + { + const svn_string_t *val; + val = svn_ra_serf__get_prop_string(wb->previous_changed_props, + wb->path, ns, name); + if (val) + { + *have_old_val = TRUE; + *old_val_p = val; + } + } + + if (wb->previous_removed_props) + { + const svn_string_t *val; + val = svn_ra_serf__get_prop_string(wb->previous_removed_props, + wb->path, ns, name); + if (val) + { + *have_old_val = TRUE; + *old_val_p = NULL; + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +proppatch_walker(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + walker_baton_t *wb = baton; + serf_bucket_t *body_bkt = wb->body_bkt; + serf_bucket_t *cdata_bkt; + serf_bucket_alloc_t *alloc; + const char *encoding; + svn_boolean_t have_old_val; + const svn_string_t *old_val; + const svn_string_t *encoded_value; + const char *prop_name; + + SVN_ERR(derive_old_val(&have_old_val, &old_val, wb, ns, name)); + + /* Jump through hoops to work with D:remove and its val = (""-for-NULL) + * representation. */ + if (wb->filter != filter_all_props) + { + if (wb->filter == filter_props_with_old_value && ! have_old_val) + return SVN_NO_ERROR; + if (wb->filter == filter_props_without_old_value && have_old_val) + return SVN_NO_ERROR; + } + if (wb->deleting) + val = NULL; + + alloc = body_bkt->allocator; + + SVN_ERR(get_encoding_and_cdata(&encoding, &encoded_value, alloc, val, + wb->body_pool, scratch_pool)); + if (encoded_value) + { + cdata_bkt = SERF_BUCKET_SIMPLE_STRING_LEN(encoded_value->data, + encoded_value->len, + alloc); + } + else + { + cdata_bkt = NULL; + } + + /* Use the namespace prefix instead of adding the xmlns attribute to support + property names containing ':' */ + if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0) + prop_name = apr_pstrcat(wb->body_pool, "S:", name, (char *)NULL); + else if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0) + prop_name = apr_pstrcat(wb->body_pool, "C:", name, (char *)NULL); + + if (cdata_bkt) + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, + "V:encoding", encoding, + NULL); + else + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, prop_name, + "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", + NULL); + + if (have_old_val) + { + const char *encoding2; + const svn_string_t *encoded_value2; + serf_bucket_t *cdata_bkt2; + + SVN_ERR(get_encoding_and_cdata(&encoding2, &encoded_value2, + alloc, old_val, + wb->body_pool, scratch_pool)); + + if (encoded_value2) + { + cdata_bkt2 = SERF_BUCKET_SIMPLE_STRING_LEN(encoded_value2->data, + encoded_value2->len, + alloc); + } + else + { + cdata_bkt2 = NULL; + } + + if (cdata_bkt2) + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE, + "V:encoding", encoding2, + NULL); + else + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE, + "V:" SVN_DAV__OLD_VALUE__ABSENT, "1", + NULL); + + if (cdata_bkt2) + serf_bucket_aggregate_append(body_bkt, cdata_bkt2); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, + "V:" SVN_DAV__OLD_VALUE); + } + if (cdata_bkt) + serf_bucket_aggregate_append(body_bkt, cdata_bkt); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, prop_name); + + return SVN_NO_ERROR; +} + +/* Possible add the lock-token "If:" precondition header to HEADERS if + an examination of COMMIT_CTX and RELPATH indicates that this is the + right thing to do. + + Generally speaking, if the client provided a lock token for + RELPATH, it's the right thing to do. There is a notable instance + where this is not the case, however. If the file at RELPATH was + explicitly deleted in this commit already, then mod_dav removed its + lock token when it fielded the DELETE request, so we don't want to + set the lock precondition again. (See + http://subversion.tigris.org/issues/show_bug.cgi?id=3674 for details.) +*/ +static svn_error_t * +maybe_set_lock_token_header(serf_bucket_t *headers, + commit_context_t *commit_ctx, + const char *relpath, + apr_pool_t *pool) +{ + const char *token; + + if (! (relpath && commit_ctx->lock_tokens)) + return SVN_NO_ERROR; + + if (! svn_hash_gets(commit_ctx->deleted_entries, relpath)) + { + token = svn_hash_gets(commit_ctx->lock_tokens, relpath); + if (token) + { + const char *token_header; + const char *token_uri; + apr_uri_t uri = commit_ctx->session->session_url; + + /* Supplying the optional URI affects apache response when + the lock is broken, see issue 4369. When present any URI + must be absolute (RFC 2518 9.4). */ + uri.path = (char *)svn_path_url_add_component2(uri.path, relpath, + pool); + token_uri = apr_uri_unparse(pool, &uri, 0); + + token_header = apr_pstrcat(pool, "<", token_uri, "> (<", token, ">)", + (char *)NULL); + serf_bucket_headers_set(headers, "If", token_header); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_proppatch_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + proppatch_context_t *proppatch = baton; + + if (SVN_IS_VALID_REVNUM(proppatch->base_revision)) + { + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_psprintf(pool, "%ld", + proppatch->base_revision)); + } + + SVN_ERR(maybe_set_lock_token_header(headers, proppatch->commit, + proppatch->relpath, pool)); + + return SVN_NO_ERROR; +} + + +struct proppatch_body_baton_t { + proppatch_context_t *proppatch; + + /* Content in the body should be allocated here, to live long enough. */ + apr_pool_t *body_pool; +}; + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_proppatch_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *scratch_pool) +{ + struct proppatch_body_baton_t *pbb = baton; + proppatch_context_t *ctx = pbb->proppatch; + serf_bucket_t *body_bkt; + walker_baton_t wb = { 0 }; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body_bkt, alloc); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:propertyupdate", + "xmlns:D", "DAV:", + "xmlns:V", SVN_DAV_PROP_NS_DAV, + "xmlns:C", SVN_DAV_PROP_NS_CUSTOM, + "xmlns:S", SVN_DAV_PROP_NS_SVN, + NULL); + + wb.body_bkt = body_bkt; + wb.body_pool = pbb->body_pool; + wb.previous_changed_props = ctx->previous_changed_props; + wb.previous_removed_props = ctx->previous_removed_props; + wb.path = ctx->path; + + if (apr_hash_count(ctx->changed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_all_props; + wb.deleting = FALSE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->changed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:set"); + } + + if (apr_hash_count(ctx->removed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:set", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_props_with_old_value; + wb.deleting = TRUE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->removed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:set"); + } + + if (apr_hash_count(ctx->removed_props) > 0) + { + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:remove", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + + wb.filter = filter_props_without_old_value; + wb.deleting = TRUE; + SVN_ERR(svn_ra_serf__walk_all_props(ctx->removed_props, ctx->path, + SVN_INVALID_REVNUM, + proppatch_walker, &wb, + scratch_pool)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:remove"); + } + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:propertyupdate"); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + +static svn_error_t* +proppatch_resource(proppatch_context_t *proppatch, + commit_context_t *commit, + apr_pool_t *pool) +{ + svn_ra_serf__handler_t *handler; + struct proppatch_body_baton_t pbb; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "PROPPATCH"; + handler->path = proppatch->path; + handler->conn = commit->conn; + handler->session = commit->session; + + handler->header_delegate = setup_proppatch_headers; + handler->header_delegate_baton = proppatch; + + pbb.proppatch = proppatch; + pbb.body_pool = pool; + handler->body_delegate = create_proppatch_body; + handler->body_delegate_baton = &pbb; + + handler->response_handler = svn_ra_serf__handle_multistatus_only; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + if (handler->sline.code != 207 + || (handler->server_error != NULL + && handler->server_error->error != NULL)) + { + return svn_error_create( + SVN_ERR_RA_DAV_PROPPATCH_FAILED, + return_response_err(handler), + _("At least one property change failed; repository" + " is unchanged")); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_put_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + file_context_t *ctx = baton; + apr_off_t offset; + + /* We need to flush the file, make it unbuffered (so that it can be + * zero-copied via mmap), and reset the position before attempting to + * deliver the file. + * + * N.B. If we have APR 1.3+, we can unbuffer the file to let us use mmap + * and zero-copy the PUT body. However, on older APR versions, we can't + * check the buffer status; but serf will fall through and create a file + * bucket for us on the buffered svndiff handle. + */ + apr_file_flush(ctx->svndiff); +#if APR_VERSION_AT_LEAST(1, 3, 0) + apr_file_buffer_set(ctx->svndiff, NULL, 0); +#endif + offset = 0; + apr_file_seek(ctx->svndiff, APR_SET, &offset); + + *body_bkt = serf_bucket_file_create(ctx->svndiff, alloc); + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_empty_put_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + *body_bkt = SERF_BUCKET_SIMPLE_STRING("", alloc); + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_put_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + file_context_t *ctx = baton; + + if (SVN_IS_VALID_REVNUM(ctx->base_revision)) + { + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_psprintf(pool, "%ld", ctx->base_revision)); + } + + if (ctx->base_checksum) + { + serf_bucket_headers_set(headers, SVN_DAV_BASE_FULLTEXT_MD5_HEADER, + ctx->base_checksum); + } + + if (ctx->result_checksum) + { + serf_bucket_headers_set(headers, SVN_DAV_RESULT_FULLTEXT_MD5_HEADER, + ctx->result_checksum); + } + + SVN_ERR(maybe_set_lock_token_header(headers, ctx->commit, + ctx->relpath, pool)); + + return APR_SUCCESS; +} + +static svn_error_t * +setup_copy_file_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + file_context_t *file = baton; + apr_uri_t uri; + const char *absolute_uri; + + /* The Dest URI must be absolute. Bummer. */ + uri = file->commit->session->session_url; + uri.path = (char*)file->url; + absolute_uri = apr_uri_unparse(pool, &uri, 0); + + serf_bucket_headers_set(headers, "Destination", absolute_uri); + + serf_bucket_headers_setn(headers, "Depth", "0"); + serf_bucket_headers_setn(headers, "Overwrite", "T"); + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_copy_dir_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + dir_context_t *dir = baton; + apr_uri_t uri; + const char *absolute_uri; + + /* The Dest URI must be absolute. Bummer. */ + uri = dir->commit->session->session_url; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + uri.path = (char *)dir->url; + } + else + { + uri.path = (char *)svn_path_url_add_component2( + dir->parent_dir->working_url, + dir->name, pool); + } + absolute_uri = apr_uri_unparse(pool, &uri, 0); + + serf_bucket_headers_set(headers, "Destination", absolute_uri); + + serf_bucket_headers_setn(headers, "Depth", "infinity"); + serf_bucket_headers_setn(headers, "Overwrite", "T"); + + /* Implicitly checkout this dir now. */ + dir->working_url = apr_pstrdup(dir->pool, uri.path); + + return SVN_NO_ERROR; +} + +static svn_error_t * +setup_delete_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + delete_context_t *ctx = baton; + + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_ltoa(pool, ctx->revision)); + + if (ctx->lock_token_hash) + { + ctx->lock_token = svn_hash_gets(ctx->lock_token_hash, ctx->path); + + if (ctx->lock_token) + { + const char *token_header; + + token_header = apr_pstrcat(pool, "<", ctx->path, "> (<", + ctx->lock_token, ">)", (char *)NULL); + + serf_bucket_headers_set(headers, "If", token_header); + + if (ctx->keep_locks) + serf_bucket_headers_setn(headers, SVN_DAV_OPTIONS_HEADER, + SVN_DAV_OPTION_KEEP_LOCKS); + } + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_delete_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + delete_context_t *ctx = baton; + serf_bucket_t *body; + + body = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body, alloc); + + svn_ra_serf__merge_lock_token_list(ctx->lock_token_hash, ctx->path, + body, alloc, pool); + + *body_bkt = body; + return SVN_NO_ERROR; +} + +/* Helper function to write the svndiff stream to temporary file. */ +static svn_error_t * +svndiff_stream_write(void *file_baton, + const char *data, + apr_size_t *len) +{ + file_context_t *ctx = file_baton; + apr_status_t status; + + status = apr_file_write_full(ctx->svndiff, data, *len, NULL); + if (status) + return svn_error_wrap_apr(status, _("Failed writing updated file")); + + return SVN_NO_ERROR; +} + + + +/* POST against 'me' resource handlers. */ + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_txn_post_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + apr_hash_t *revprops = baton; + svn_skel_t *request_skel; + svn_stringbuf_t *skel_str; + + request_skel = svn_skel__make_empty_list(pool); + if (revprops) + { + svn_skel_t *proplist_skel; + + SVN_ERR(svn_skel__unparse_proplist(&proplist_skel, revprops, pool)); + svn_skel__prepend(proplist_skel, request_skel); + svn_skel__prepend_str("create-txn-with-props", request_skel, pool); + skel_str = svn_skel__unparse(request_skel, pool); + *body_bkt = SERF_BUCKET_SIMPLE_STRING(skel_str->data, alloc); + } + else + { + *body_bkt = SERF_BUCKET_SIMPLE_STRING("( create-txn )", alloc); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_header_delegate_t */ +static svn_error_t * +setup_post_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ +#ifdef SVN_DAV_SEND_VTXN_NAME + /* Enable this to exercise the VTXN-NAME code based on a client + supplied transaction name. */ + serf_bucket_headers_set(headers, SVN_DAV_VTXN_NAME_HEADER, + svn_uuid_generate(pool)); +#endif + + return SVN_NO_ERROR; +} + + +/* Handler baton for POST request. */ +typedef struct post_response_ctx_t +{ + svn_ra_serf__handler_t *handler; + commit_context_t *commit_ctx; +} post_response_ctx_t; + + +/* This implements serf_bucket_headers_do_callback_fn_t. */ +static int +post_headers_iterator_callback(void *baton, + const char *key, + const char *val) +{ + post_response_ctx_t *prc = baton; + commit_context_t *prc_cc = prc->commit_ctx; + svn_ra_serf__session_t *sess = prc_cc->session; + + /* If we provided a UUID to the POST request, we should get back + from the server an SVN_DAV_VTXN_NAME_HEADER header; otherwise we + expect the SVN_DAV_TXN_NAME_HEADER. We certainly don't expect to + see both. */ + + if (svn_cstring_casecmp(key, SVN_DAV_TXN_NAME_HEADER) == 0) + { + /* Build out txn and txn-root URLs using the txn name we're + given, and store the whole lot of it in the commit context. */ + prc_cc->txn_url = + svn_path_url_add_component2(sess->txn_stub, val, prc_cc->pool); + prc_cc->txn_root_url = + svn_path_url_add_component2(sess->txn_root_stub, val, prc_cc->pool); + } + + if (svn_cstring_casecmp(key, SVN_DAV_VTXN_NAME_HEADER) == 0) + { + /* Build out vtxn and vtxn-root URLs using the vtxn name we're + given, and store the whole lot of it in the commit context. */ + prc_cc->txn_url = + svn_path_url_add_component2(sess->vtxn_stub, val, prc_cc->pool); + prc_cc->txn_root_url = + svn_path_url_add_component2(sess->vtxn_root_stub, val, prc_cc->pool); + } + + return 0; +} + + +/* A custom serf_response_handler_t which is mostly a wrapper around + svn_ra_serf__expect_empty_body -- it just notices POST response + headers, too. + + Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +post_response_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + post_response_ctx_t *prc = baton; + serf_bucket_t *hdrs = serf_bucket_response_get_headers(response); + + /* Then see which ones we can discover. */ + serf_bucket_headers_do(hdrs, post_headers_iterator_callback, prc); + + /* Execute the 'real' response handler to XML-parse the repsonse body. */ + return svn_ra_serf__expect_empty_body(request, response, + prc->handler, scratch_pool); +} + + + +/* Commit baton callbacks */ + +static svn_error_t * +open_root(void *edit_baton, + svn_revnum_t base_revision, + apr_pool_t *dir_pool, + void **root_baton) +{ + commit_context_t *ctx = edit_baton; + svn_ra_serf__handler_t *handler; + proppatch_context_t *proppatch_ctx; + dir_context_t *dir; + apr_hash_index_t *hi; + const char *proppatch_target = NULL; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(ctx->session)) + { + post_response_ctx_t *prc; + const char *rel_path; + svn_boolean_t post_with_revprops + = (NULL != svn_hash_gets(ctx->session->supported_posts, + "create-txn-with-props")); + + /* Create our activity URL now on the server. */ + handler = apr_pcalloc(ctx->pool, sizeof(*handler)); + handler->handler_pool = ctx->pool; + handler->method = "POST"; + handler->body_type = SVN_SKEL_MIME_TYPE; + handler->body_delegate = create_txn_post_body; + handler->body_delegate_baton = + post_with_revprops ? ctx->revprop_table : NULL; + handler->header_delegate = setup_post_headers; + handler->header_delegate_baton = NULL; + handler->path = ctx->session->me_resource; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + prc = apr_pcalloc(ctx->pool, sizeof(*prc)); + prc->handler = handler; + prc->commit_ctx = ctx; + + handler->response_handler = post_response_handler; + handler->response_baton = prc; + + SVN_ERR(svn_ra_serf__context_run_one(handler, ctx->pool)); + + if (handler->sline.code != 201) + { + apr_status_t status = SVN_ERR_RA_DAV_REQUEST_FAILED; + + switch (handler->sline.code) + { + case 403: + status = SVN_ERR_RA_DAV_FORBIDDEN; + break; + case 404: + status = SVN_ERR_FS_NOT_FOUND; + break; + } + + return svn_error_createf(status, NULL, + _("%s of '%s': %d %s (%s://%s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason, + ctx->session->session_url.scheme, + ctx->session->session_url.hostinfo); + } + if (! (ctx->txn_root_url && ctx->txn_url)) + { + return svn_error_createf( + SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("POST request did not return transaction information")); + } + + /* Fixup the txn_root_url to point to the anchor of the commit. */ + SVN_ERR(svn_ra_serf__get_relative_path(&rel_path, + ctx->session->session_url.path, + ctx->session, NULL, dir_pool)); + ctx->txn_root_url = svn_path_url_add_component2(ctx->txn_root_url, + rel_path, ctx->pool); + + /* Build our directory baton. */ + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + dir->pool = dir_pool; + dir->commit = ctx; + dir->base_revision = base_revision; + dir->relpath = ""; + dir->name = ""; + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + dir->url = apr_pstrdup(dir->pool, ctx->txn_root_url); + + /* If we included our revprops in the POST, we need not + PROPPATCH them. */ + proppatch_target = post_with_revprops ? NULL : ctx->txn_url; + } + else + { + const char *activity_str = ctx->session->activity_collection_url; + + if (!activity_str) + SVN_ERR(svn_ra_serf__v1_get_activity_collection(&activity_str, + ctx->session->conns[0], + ctx->pool, + ctx->pool)); + + /* Cache the result. */ + if (activity_str) + { + ctx->session->activity_collection_url = + apr_pstrdup(ctx->session->pool, activity_str); + } + else + { + return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("The OPTIONS response did not include the " + "requested activity-collection-set value")); + } + + ctx->activity_url = + svn_path_url_add_component2(activity_str, svn_uuid_generate(ctx->pool), + ctx->pool); + + /* Create our activity URL now on the server. */ + handler = apr_pcalloc(ctx->pool, sizeof(*handler)); + handler->handler_pool = ctx->pool; + handler->method = "MKACTIVITY"; + handler->path = ctx->activity_url; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, ctx->pool)); + + if (handler->sline.code != 201) + { + apr_status_t status = SVN_ERR_RA_DAV_REQUEST_FAILED; + + switch (handler->sline.code) + { + case 403: + status = SVN_ERR_RA_DAV_FORBIDDEN; + break; + case 404: + status = SVN_ERR_FS_NOT_FOUND; + break; + } + + return svn_error_createf(status, NULL, + _("%s of '%s': %d %s (%s://%s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason, + ctx->session->session_url.scheme, + ctx->session->session_url.hostinfo); + } + + /* Now go fetch our VCC and baseline so we can do a CHECKOUT. */ + SVN_ERR(svn_ra_serf__discover_vcc(&(ctx->vcc_url), ctx->session, + ctx->conn, ctx->pool)); + + + /* Build our directory baton. */ + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + dir->pool = dir_pool; + dir->commit = ctx; + dir->base_revision = base_revision; + dir->relpath = ""; + dir->name = ""; + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + SVN_ERR(get_version_url(&dir->url, dir->commit->session, + dir->relpath, + dir->base_revision, ctx->checked_in_url, + dir->pool, dir->pool /* scratch_pool */)); + ctx->checked_in_url = dir->url; + + /* Checkout our root dir */ + SVN_ERR(checkout_dir(dir, dir->pool /* scratch_pool */)); + + proppatch_target = ctx->baseline_url; + } + + /* Unless this is NULL -- which means we don't need to PROPPATCH the + transaction with our revprops -- then, you know, PROPPATCH the + transaction with our revprops. */ + if (proppatch_target) + { + proppatch_ctx = apr_pcalloc(ctx->pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = dir_pool; + proppatch_ctx->commit = ctx; + proppatch_ctx->path = proppatch_target; + proppatch_ctx->changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->removed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->base_revision = SVN_INVALID_REVNUM; + + for (hi = apr_hash_first(ctx->pool, ctx->revprop_table); hi; + hi = apr_hash_next(hi)) + { + const char *name = svn__apr_hash_index_key(hi); + svn_string_t *value = svn__apr_hash_index_val(hi); + const char *ns; + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + svn_ra_serf__set_prop(proppatch_ctx->changed_props, + proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + + SVN_ERR(proppatch_resource(proppatch_ctx, dir->commit, ctx->pool)); + } + + *root_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_entry(const char *path, + svn_revnum_t revision, + void *parent_baton, + apr_pool_t *pool) +{ + dir_context_t *dir = parent_baton; + delete_context_t *delete_ctx; + svn_ra_serf__handler_t *handler; + const char *delete_target; + svn_error_t *err; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + delete_target = svn_path_url_add_component2(dir->commit->txn_root_url, + path, dir->pool); + } + else + { + /* Ensure our directory has been checked out */ + SVN_ERR(checkout_dir(dir, pool /* scratch_pool */)); + delete_target = svn_path_url_add_component2(dir->working_url, + svn_relpath_basename(path, + NULL), + pool); + } + + /* DELETE our entry */ + delete_ctx = apr_pcalloc(pool, sizeof(*delete_ctx)); + delete_ctx->path = apr_pstrdup(pool, path); + delete_ctx->revision = revision; + delete_ctx->lock_token_hash = dir->commit->lock_tokens; + delete_ctx->keep_locks = dir->commit->keep_locks; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->session = dir->commit->session; + handler->conn = dir->commit->conn; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + handler->header_delegate = setup_delete_headers; + handler->header_delegate_baton = delete_ctx; + + handler->method = "DELETE"; + handler->path = delete_target; + + err = svn_ra_serf__context_run_one(handler, pool); + + if (err && + (err->apr_err == SVN_ERR_FS_BAD_LOCK_TOKEN || + err->apr_err == SVN_ERR_FS_NO_LOCK_TOKEN || + err->apr_err == SVN_ERR_FS_LOCK_OWNER_MISMATCH || + err->apr_err == SVN_ERR_FS_PATH_ALREADY_LOCKED)) + { + svn_error_clear(err); + + /* An error has been registered on the connection. Reset the thing + so that we can use it again. */ + serf_connection_reset(handler->conn->conn); + + handler->body_delegate = create_delete_body; + handler->body_delegate_baton = delete_ctx; + handler->body_type = "text/xml"; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + } + else if (err) + { + return err; + } + + /* 204 No Content: item successfully deleted */ + if (handler->sline.code != 204) + { + return svn_error_trace(return_response_err(handler)); + } + + svn_hash_sets(dir->commit->deleted_entries, + apr_pstrdup(dir->commit->pool, path), (void *)1); + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_directory(const char *path, + void *parent_baton, + const char *copyfrom_path, + svn_revnum_t copyfrom_revision, + apr_pool_t *dir_pool, + void **child_baton) +{ + dir_context_t *parent = parent_baton; + dir_context_t *dir; + svn_ra_serf__handler_t *handler; + apr_status_t status; + const char *mkcol_target; + + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + + dir->pool = dir_pool; + dir->parent_dir = parent; + dir->commit = parent->commit; + dir->added = TRUE; + dir->base_revision = SVN_INVALID_REVNUM; + dir->copy_revision = copyfrom_revision; + dir->copy_path = copyfrom_path; + dir->relpath = apr_pstrdup(dir->pool, path); + dir->name = svn_relpath_basename(dir->relpath, NULL); + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + dir->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, dir->pool); + mkcol_target = dir->url; + } + else + { + /* Ensure our parent is checked out. */ + SVN_ERR(checkout_dir(parent, dir->pool /* scratch_pool */)); + + dir->url = svn_path_url_add_component2(parent->commit->checked_in_url, + dir->name, dir->pool); + mkcol_target = svn_path_url_add_component2( + parent->working_url, + dir->name, dir->pool); + } + + handler = apr_pcalloc(dir->pool, sizeof(*handler)); + handler->handler_pool = dir->pool; + handler->conn = dir->commit->conn; + handler->session = dir->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + if (!dir->copy_path) + { + handler->method = "MKCOL"; + handler->path = mkcol_target; + } + else + { + apr_uri_t uri; + const char *req_url; + + status = apr_uri_parse(dir->pool, dir->copy_path, &uri); + if (status) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unable to parse URL '%s'"), + dir->copy_path); + } + + /* ### conn==NULL for session->conns[0]. same as commit->conn. */ + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + dir->commit->session, + NULL /* conn */, + uri.path, dir->copy_revision, + dir_pool, dir_pool)); + + handler->method = "COPY"; + handler->path = req_url; + + handler->header_delegate = setup_copy_dir_headers; + handler->header_delegate_baton = dir; + } + + SVN_ERR(svn_ra_serf__context_run_one(handler, dir->pool)); + + switch (handler->sline.code) + { + case 201: /* Created: item was successfully copied */ + case 204: /* No Content: item successfully replaced an existing target */ + break; + + case 403: + return svn_error_createf(SVN_ERR_RA_DAV_FORBIDDEN, NULL, + _("Access to '%s' forbidden"), + handler->path); + default: + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("Adding directory failed: %s on %s " + "(%d %s)"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason); + } + + *child_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_directory(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *dir_pool, + void **child_baton) +{ + dir_context_t *parent = parent_baton; + dir_context_t *dir; + + dir = apr_pcalloc(dir_pool, sizeof(*dir)); + + dir->pool = dir_pool; + + dir->parent_dir = parent; + dir->commit = parent->commit; + + dir->added = FALSE; + dir->base_revision = base_revision; + dir->relpath = apr_pstrdup(dir->pool, path); + dir->name = svn_relpath_basename(dir->relpath, NULL); + dir->changed_props = apr_hash_make(dir->pool); + dir->removed_props = apr_hash_make(dir->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + dir->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, dir->pool); + } + else + { + SVN_ERR(get_version_url(&dir->url, + dir->commit->session, + dir->relpath, dir->base_revision, + dir->commit->checked_in_url, + dir->pool, dir->pool /* scratch_pool */)); + } + *child_baton = dir; + + return SVN_NO_ERROR; +} + +static svn_error_t * +change_dir_prop(void *dir_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + dir_context_t *dir = dir_baton; + const char *ns; + const char *proppatch_target; + + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + proppatch_target = dir->url; + } + else + { + /* Ensure we have a checked out dir. */ + SVN_ERR(checkout_dir(dir, pool /* scratch_pool */)); + + proppatch_target = dir->working_url; + } + + name = apr_pstrdup(dir->pool, name); + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + if (value) + { + value = svn_string_dup(value, dir->pool); + svn_ra_serf__set_prop(dir->changed_props, proppatch_target, + ns, name, value, dir->pool); + } + else + { + value = svn_string_create_empty(dir->pool); + svn_ra_serf__set_prop(dir->removed_props, proppatch_target, + ns, name, value, dir->pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_directory(void *dir_baton, + apr_pool_t *pool) +{ + dir_context_t *dir = dir_baton; + + /* Huh? We're going to be called before the texts are sent. Ugh. + * Therefore, just wave politely at our caller. + */ + + /* PROPPATCH our prop change and pass it along. */ + if (apr_hash_count(dir->changed_props) || + apr_hash_count(dir->removed_props)) + { + proppatch_context_t *proppatch_ctx; + + proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = pool; + proppatch_ctx->commit = dir->commit; + proppatch_ctx->relpath = dir->relpath; + proppatch_ctx->changed_props = dir->changed_props; + proppatch_ctx->removed_props = dir->removed_props; + proppatch_ctx->base_revision = dir->base_revision; + + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + proppatch_ctx->path = dir->url; + } + else + { + proppatch_ctx->path = dir->working_url; + } + + SVN_ERR(proppatch_resource(proppatch_ctx, dir->commit, dir->pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +add_file(const char *path, + void *parent_baton, + const char *copy_path, + svn_revnum_t copy_revision, + apr_pool_t *file_pool, + void **file_baton) +{ + dir_context_t *dir = parent_baton; + file_context_t *new_file; + const char *deleted_parent = path; + + new_file = apr_pcalloc(file_pool, sizeof(*new_file)); + new_file->pool = file_pool; + + dir->ref_count++; + + new_file->parent_dir = dir; + new_file->commit = dir->commit; + new_file->relpath = apr_pstrdup(new_file->pool, path); + new_file->name = svn_relpath_basename(new_file->relpath, NULL); + new_file->added = TRUE; + new_file->base_revision = SVN_INVALID_REVNUM; + new_file->copy_path = copy_path; + new_file->copy_revision = copy_revision; + new_file->changed_props = apr_hash_make(new_file->pool); + new_file->removed_props = apr_hash_make(new_file->pool); + + /* Ensure that the file doesn't exist by doing a HEAD on the + resource. If we're using HTTP v2, we'll just look into the + transaction root tree for this thing. */ + if (USING_HTTPV2_COMMIT_SUPPORT(dir->commit)) + { + new_file->url = svn_path_url_add_component2(dir->commit->txn_root_url, + path, new_file->pool); + } + else + { + /* Ensure our parent directory has been checked out */ + SVN_ERR(checkout_dir(dir, new_file->pool /* scratch_pool */)); + + new_file->url = + svn_path_url_add_component2(dir->working_url, + new_file->name, new_file->pool); + } + + while (deleted_parent && deleted_parent[0] != '\0') + { + if (svn_hash_gets(dir->commit->deleted_entries, deleted_parent)) + { + break; + } + deleted_parent = svn_relpath_dirname(deleted_parent, file_pool); + } + + if (! ((dir->added && !dir->copy_path) || + (deleted_parent && deleted_parent[0] != '\0'))) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(new_file->pool, sizeof(*handler)); + handler->handler_pool = new_file->pool; + handler->session = new_file->commit->session; + handler->conn = new_file->commit->conn; + handler->method = "HEAD"; + handler->path = svn_path_url_add_component2( + dir->commit->session->session_url.path, + path, new_file->pool); + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, new_file->pool)); + + if (handler->sline.code != 404) + { + return svn_error_createf(SVN_ERR_RA_DAV_ALREADY_EXISTS, NULL, + _("File '%s' already exists"), path); + } + } + + *file_baton = new_file; + + return SVN_NO_ERROR; +} + +static svn_error_t * +open_file(const char *path, + void *parent_baton, + svn_revnum_t base_revision, + apr_pool_t *file_pool, + void **file_baton) +{ + dir_context_t *parent = parent_baton; + file_context_t *new_file; + + new_file = apr_pcalloc(file_pool, sizeof(*new_file)); + new_file->pool = file_pool; + + parent->ref_count++; + + new_file->parent_dir = parent; + new_file->commit = parent->commit; + new_file->relpath = apr_pstrdup(new_file->pool, path); + new_file->name = svn_relpath_basename(new_file->relpath, NULL); + new_file->added = FALSE; + new_file->base_revision = base_revision; + new_file->changed_props = apr_hash_make(new_file->pool); + new_file->removed_props = apr_hash_make(new_file->pool); + + if (USING_HTTPV2_COMMIT_SUPPORT(parent->commit)) + { + new_file->url = svn_path_url_add_component2(parent->commit->txn_root_url, + path, new_file->pool); + } + else + { + /* CHECKOUT the file into our activity. */ + SVN_ERR(checkout_file(new_file, new_file->pool /* scratch_pool */)); + + new_file->url = new_file->working_url; + } + + *file_baton = new_file; + + return SVN_NO_ERROR; +} + +static svn_error_t * +apply_textdelta(void *file_baton, + const char *base_checksum, + apr_pool_t *pool, + svn_txdelta_window_handler_t *handler, + void **handler_baton) +{ + file_context_t *ctx = file_baton; + + /* Store the stream in a temporary file; we'll give it to serf when we + * close this file. + * + * TODO: There should be a way we can stream the request body instead of + * writing to a temporary file (ugh). A special svn stream serf bucket + * that returns EAGAIN until we receive the done call? But, when + * would we run through the serf context? Grr. + * + * ctx->pool is the same for all files in the commit that send a + * textdelta so this file is explicitly closed in close_file to + * avoid too many simultaneously open files. + */ + + SVN_ERR(svn_io_open_unique_file3(&ctx->svndiff, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + ctx->pool, pool)); + + ctx->stream = svn_stream_create(ctx, pool); + svn_stream_set_write(ctx->stream, svndiff_stream_write); + + svn_txdelta_to_svndiff3(handler, handler_baton, ctx->stream, 0, + SVN_DELTA_COMPRESSION_LEVEL_DEFAULT, pool); + + if (base_checksum) + ctx->base_checksum = apr_pstrdup(ctx->pool, base_checksum); + + return SVN_NO_ERROR; +} + +static svn_error_t * +change_file_prop(void *file_baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + file_context_t *file = file_baton; + const char *ns; + + name = apr_pstrdup(file->pool, name); + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + if (value) + { + value = svn_string_dup(value, file->pool); + svn_ra_serf__set_prop(file->changed_props, file->url, + ns, name, value, file->pool); + } + else + { + value = svn_string_create_empty(file->pool); + + svn_ra_serf__set_prop(file->removed_props, file->url, + ns, name, value, file->pool); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_file(void *file_baton, + const char *text_checksum, + apr_pool_t *scratch_pool) +{ + file_context_t *ctx = file_baton; + svn_boolean_t put_empty_file = FALSE; + apr_status_t status; + + ctx->result_checksum = text_checksum; + + if (ctx->copy_path) + { + svn_ra_serf__handler_t *handler; + apr_uri_t uri; + const char *req_url; + + status = apr_uri_parse(scratch_pool, ctx->copy_path, &uri); + if (status) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unable to parse URL '%s'"), + ctx->copy_path); + } + + /* ### conn==NULL for session->conns[0]. same as commit->conn. */ + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + ctx->commit->session, + NULL /* conn */, + uri.path, ctx->copy_revision, + scratch_pool, scratch_pool)); + + handler = apr_pcalloc(scratch_pool, sizeof(*handler)); + handler->handler_pool = scratch_pool; + handler->method = "COPY"; + handler->path = req_url; + handler->conn = ctx->commit->conn; + handler->session = ctx->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + handler->header_delegate = setup_copy_file_headers; + handler->header_delegate_baton = ctx; + + SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); + + if (handler->sline.code != 201 && handler->sline.code != 204) + { + return svn_error_trace(return_response_err(handler)); + } + } + + /* If we got no stream of changes, but this is an added-without-history + * file, make a note that we'll be PUTting a zero-byte file to the server. + */ + if ((!ctx->stream) && ctx->added && (!ctx->copy_path)) + put_empty_file = TRUE; + + /* If we had a stream of changes, push them to the server... */ + if (ctx->stream || put_empty_file) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(scratch_pool, sizeof(*handler)); + handler->handler_pool = scratch_pool; + handler->method = "PUT"; + handler->path = ctx->url; + handler->conn = ctx->commit->conn; + handler->session = ctx->commit->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + if (put_empty_file) + { + handler->body_delegate = create_empty_put_body; + handler->body_delegate_baton = ctx; + handler->body_type = "text/plain"; + } + else + { + handler->body_delegate = create_put_body; + handler->body_delegate_baton = ctx; + handler->body_type = SVN_SVNDIFF_MIME_TYPE; + } + + handler->header_delegate = setup_put_headers; + handler->header_delegate_baton = ctx; + + SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); + + if (handler->sline.code != 204 && handler->sline.code != 201) + { + return svn_error_trace(return_response_err(handler)); + } + } + + if (ctx->svndiff) + SVN_ERR(svn_io_file_close(ctx->svndiff, scratch_pool)); + + /* If we had any prop changes, push them via PROPPATCH. */ + if (apr_hash_count(ctx->changed_props) || + apr_hash_count(ctx->removed_props)) + { + proppatch_context_t *proppatch; + + proppatch = apr_pcalloc(ctx->pool, sizeof(*proppatch)); + proppatch->pool = ctx->pool; + proppatch->relpath = ctx->relpath; + proppatch->path = ctx->url; + proppatch->commit = ctx->commit; + proppatch->changed_props = ctx->changed_props; + proppatch->removed_props = ctx->removed_props; + proppatch->base_revision = ctx->base_revision; + + SVN_ERR(proppatch_resource(proppatch, ctx->commit, ctx->pool)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_edit(void *edit_baton, + apr_pool_t *pool) +{ + commit_context_t *ctx = edit_baton; + const char *merge_target = + ctx->activity_url ? ctx->activity_url : ctx->txn_url; + const svn_commit_info_t *commit_info; + int response_code; + + /* MERGE our activity */ + SVN_ERR(svn_ra_serf__run_merge(&commit_info, &response_code, + ctx->session, + ctx->session->conns[0], + merge_target, + ctx->lock_tokens, + ctx->keep_locks, + pool, pool)); + + if (response_code != 200) + { + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("MERGE request failed: returned %d " + "(during commit)"), + response_code); + } + + /* Inform the WC that we did a commit. */ + if (ctx->callback) + SVN_ERR(ctx->callback(commit_info, ctx->callback_baton, pool)); + + /* If we're using activities, DELETE our completed activity. */ + if (ctx->activity_url) + { + svn_ra_serf__handler_t *handler; + + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "DELETE"; + handler->path = ctx->activity_url; + handler->conn = ctx->conn; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + SVN_ERR_ASSERT(handler->sline.code == 204); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +abort_edit(void *edit_baton, + apr_pool_t *pool) +{ + commit_context_t *ctx = edit_baton; + svn_ra_serf__handler_t *handler; + + /* If an activity or transaction wasn't even created, don't bother + trying to delete it. */ + if (! (ctx->activity_url || ctx->txn_url)) + return SVN_NO_ERROR; + + /* An error occurred on conns[0]. serf 0.4.0 remembers that the connection + had a problem. We need to reset it, in order to use it again. */ + serf_connection_reset(ctx->session->conns[0]->conn); + + /* DELETE our aborted activity */ + handler = apr_pcalloc(pool, sizeof(*handler)); + handler->handler_pool = pool; + handler->method = "DELETE"; + handler->conn = ctx->session->conns[0]; + handler->session = ctx->session; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + if (USING_HTTPV2_COMMIT_SUPPORT(ctx)) /* HTTP v2 */ + handler->path = ctx->txn_url; + else + handler->path = ctx->activity_url; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + /* 204 if deleted, + 403 if DELETE was forbidden (indicates MKACTIVITY was forbidden too), + 404 if the activity wasn't found. */ + if (handler->sline.code != 204 + && handler->sline.code != 403 + && handler->sline.code != 404 + ) + { + SVN_ERR_MALFUNCTION(); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_commit_editor(svn_ra_session_t *ra_session, + const svn_delta_editor_t **ret_editor, + void **edit_baton, + apr_hash_t *revprop_table, + svn_commit_callback2_t callback, + void *callback_baton, + apr_hash_t *lock_tokens, + svn_boolean_t keep_locks, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + svn_delta_editor_t *editor; + commit_context_t *ctx; + const char *repos_root; + const char *base_relpath; + svn_boolean_t supports_ephemeral_props; + + ctx = apr_pcalloc(pool, sizeof(*ctx)); + + ctx->pool = pool; + + ctx->session = session; + ctx->conn = session->conns[0]; + + ctx->revprop_table = svn_prop_hash_dup(revprop_table, pool); + + /* If the server supports ephemeral properties, add some carrying + interesting version information. */ + SVN_ERR(svn_ra_serf__has_capability(ra_session, &supports_ephemeral_props, + SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS, + pool)); + if (supports_ephemeral_props) + { + svn_hash_sets(ctx->revprop_table, + apr_pstrdup(pool, SVN_PROP_TXN_CLIENT_COMPAT_VERSION), + svn_string_create(SVN_VER_NUMBER, pool)); + svn_hash_sets(ctx->revprop_table, + apr_pstrdup(pool, SVN_PROP_TXN_USER_AGENT), + svn_string_create(session->useragent, pool)); + } + + ctx->callback = callback; + ctx->callback_baton = callback_baton; + + ctx->lock_tokens = lock_tokens; + ctx->keep_locks = keep_locks; + + ctx->deleted_entries = apr_hash_make(ctx->pool); + + editor = svn_delta_default_editor(pool); + editor->open_root = open_root; + editor->delete_entry = delete_entry; + editor->add_directory = add_directory; + editor->open_directory = open_directory; + editor->change_dir_prop = change_dir_prop; + editor->close_directory = close_directory; + editor->add_file = add_file; + editor->open_file = open_file; + editor->apply_textdelta = apply_textdelta; + editor->change_file_prop = change_file_prop; + editor->close_file = close_file; + editor->close_edit = close_edit; + editor->abort_edit = abort_edit; + + *ret_editor = editor; + *edit_baton = ctx; + + SVN_ERR(svn_ra_serf__get_repos_root(ra_session, &repos_root, pool)); + base_relpath = svn_uri_skip_ancestor(repos_root, session->session_url_str, + pool); + + SVN_ERR(svn_editor__insert_shims(ret_editor, edit_baton, *ret_editor, + *edit_baton, repos_root, base_relpath, + session->shim_callbacks, pool, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__change_rev_prop(svn_ra_session_t *ra_session, + svn_revnum_t rev, + const char *name, + const svn_string_t *const *old_value_p, + const svn_string_t *value, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + proppatch_context_t *proppatch_ctx; + commit_context_t *commit; + const char *proppatch_target; + const char *ns; + svn_error_t *err; + + if (old_value_p) + { + svn_boolean_t capable; + SVN_ERR(svn_ra_serf__has_capability(ra_session, &capable, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + pool)); + + /* How did you get past the same check in svn_ra_change_rev_prop2()? */ + SVN_ERR_ASSERT(capable); + } + + commit = apr_pcalloc(pool, sizeof(*commit)); + + commit->pool = pool; + + commit->session = session; + commit->conn = session->conns[0]; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + { + proppatch_target = apr_psprintf(pool, "%s/%ld", session->rev_stub, rev); + } + else + { + const char *vcc_url; + + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, commit->session, + commit->conn, pool)); + + SVN_ERR(svn_ra_serf__fetch_dav_prop(&proppatch_target, + commit->conn, vcc_url, rev, + "href", + pool, pool)); + } + + if (strncmp(name, SVN_PROP_PREFIX, sizeof(SVN_PROP_PREFIX) - 1) == 0) + { + ns = SVN_DAV_PROP_NS_SVN; + name += sizeof(SVN_PROP_PREFIX) - 1; + } + else + { + ns = SVN_DAV_PROP_NS_CUSTOM; + } + + /* PROPPATCH our log message and pass it along. */ + proppatch_ctx = apr_pcalloc(pool, sizeof(*proppatch_ctx)); + proppatch_ctx->pool = pool; + proppatch_ctx->commit = commit; + proppatch_ctx->path = proppatch_target; + proppatch_ctx->changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->removed_props = apr_hash_make(proppatch_ctx->pool); + if (old_value_p) + { + proppatch_ctx->previous_changed_props = apr_hash_make(proppatch_ctx->pool); + proppatch_ctx->previous_removed_props = apr_hash_make(proppatch_ctx->pool); + } + proppatch_ctx->base_revision = SVN_INVALID_REVNUM; + + if (old_value_p && *old_value_p) + { + svn_ra_serf__set_prop(proppatch_ctx->previous_changed_props, + proppatch_ctx->path, + ns, name, *old_value_p, proppatch_ctx->pool); + } + else if (old_value_p) + { + svn_string_t *dummy_value = svn_string_create_empty(proppatch_ctx->pool); + + svn_ra_serf__set_prop(proppatch_ctx->previous_removed_props, + proppatch_ctx->path, + ns, name, dummy_value, proppatch_ctx->pool); + } + + if (value) + { + svn_ra_serf__set_prop(proppatch_ctx->changed_props, proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + else + { + value = svn_string_create_empty(proppatch_ctx->pool); + + svn_ra_serf__set_prop(proppatch_ctx->removed_props, proppatch_ctx->path, + ns, name, value, proppatch_ctx->pool); + } + + err = proppatch_resource(proppatch_ctx, commit, proppatch_ctx->pool); + if (err) + return + svn_error_create + (SVN_ERR_RA_DAV_REQUEST_FAILED, err, + _("DAV request failed; it's possible that the repository's " + "pre-revprop-change hook either failed or is non-existent")); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/get_deleted_rev.c b/subversion/libsvn_ra_serf/get_deleted_rev.c new file mode 100644 index 0000000..40f6b1d --- /dev/null +++ b/subversion/libsvn_ra_serf/get_deleted_rev.c @@ -0,0 +1,178 @@ +/* + * get_deleted_rev.c : ra_serf get_deleted_rev API implementation. + * + * ==================================================================== + * 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_ra.h" +#include "svn_xml.h" +#include "svn_path.h" +#include "svn_private_config.h" + +#include "../libsvn_ra/ra_loader.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum drev_state_e { + INITIAL = 0, + REPORT, + VERSION_NAME +}; + +typedef struct drev_context_t { + const char *path; + svn_revnum_t peg_revision; + svn_revnum_t end_revision; + + /* What revision was PATH@PEG_REVISION first deleted within + the range PEG_REVISION-END-END_REVISION? */ + svn_revnum_t *revision_deleted; + +} drev_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t getdrev_ttable[] = { + { INITIAL, S_, "get-deleted-rev-report", REPORT, + FALSE, { NULL }, FALSE }, + + { REPORT, D_, SVN_DAV__VERSION_NAME, VERSION_NAME, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +getdrev_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + drev_context_t *drev_ctx = baton; + + SVN_ERR_ASSERT(leaving_state == VERSION_NAME); + SVN_ERR_ASSERT(cdata != NULL); + + *drev_ctx->revision_deleted = SVN_STR_TO_REV(cdata->data); + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_getdrev_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + drev_context_t *drev_ctx = baton; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, + "S:get-deleted-rev-report", + "xmlns:S", SVN_XML_NAMESPACE, + "xmlns:D", "DAV:", + NULL, NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "S:path", drev_ctx->path, + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:peg-revision", + apr_ltoa(pool, drev_ctx->peg_revision), + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:end-revision", + apr_ltoa(pool, drev_ctx->end_revision), + alloc); + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, + "S:get-deleted-rev-report"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_deleted_rev(svn_ra_session_t *session, + const char *path, + svn_revnum_t peg_revision, + svn_revnum_t end_revision, + svn_revnum_t *revision_deleted, + apr_pool_t *pool) +{ + drev_context_t *drev_ctx; + svn_ra_serf__session_t *ras = session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url; + svn_error_t *err; + + drev_ctx = apr_pcalloc(pool, sizeof(*drev_ctx)); + drev_ctx->path = path; + drev_ctx->peg_revision = peg_revision; + drev_ctx->end_revision = end_revision; + drev_ctx->revision_deleted = revision_deleted; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + ras, NULL /* conn */, + NULL /* url */, peg_revision, + pool, pool)); + + xmlctx = svn_ra_serf__xml_context_create(getdrev_ttable, + NULL, getdrev_closed, NULL, + drev_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_type = "text/xml"; + handler->body_delegate = create_getdrev_body; + handler->body_delegate_baton = drev_ctx; + handler->conn = ras->conns[0]; + handler->session = ras; + + err = svn_ra_serf__context_run_one(handler, pool); + + /* Map status 501: Method Not Implemented to our not implemented error. + 1.5.x servers and older don't support this report. */ + if (handler->sline.code == 501) + return svn_error_createf(SVN_ERR_RA_NOT_IMPLEMENTED, err, + _("'%s' REPORT not implemented"), + "get-deleted-rev"); + SVN_ERR(err); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/getdate.c b/subversion/libsvn_ra_serf/getdate.c new file mode 100644 index 0000000..867e86f --- /dev/null +++ b/subversion/libsvn_ra_serf/getdate.c @@ -0,0 +1,161 @@ +/* + * getdate.c : entry point for get_dated_revision for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_time.h" +#include "svn_xml.h" + +#include "private/svn_dav_protocol.h" + +#include "svn_private_config.h" + +#include "../libsvn_ra/ra_loader.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum date_state_e { + INITIAL = 0, + REPORT, + VERSION_NAME +}; + + +typedef struct date_context_t { + /* The time asked about. */ + apr_time_t time; + + /* What was the youngest revision at that time? */ + svn_revnum_t *revision; + +} date_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t date_ttable[] = { + { INITIAL, S_, "dated-rev-report", REPORT, + FALSE, { NULL }, FALSE }, + + { REPORT, D_, SVN_DAV__VERSION_NAME, VERSION_NAME, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +date_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + date_context_t *date_ctx = baton; + + SVN_ERR_ASSERT(leaving_state == VERSION_NAME); + SVN_ERR_ASSERT(cdata != NULL); + + *date_ctx->revision = SVN_STR_TO_REV(cdata->data); + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_getdate_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + date_context_t *date_ctx = baton; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "S:dated-rev-report", + "xmlns:S", SVN_XML_NAMESPACE, + "xmlns:D", "DAV:", + NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "D:" SVN_DAV__CREATIONDATE, + svn_time_to_cstring(date_ctx->time, pool), + alloc); + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "S:dated-rev-report"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_dated_revision(svn_ra_session_t *ra_session, + svn_revnum_t *revision, + apr_time_t tm, + apr_pool_t *pool) +{ + date_context_t *date_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *report_target; + + date_ctx = apr_palloc(pool, sizeof(*date_ctx)); + date_ctx->time = tm; + date_ctx->revision = revision; + + SVN_ERR(svn_ra_serf__report_resource(&report_target, session, NULL, pool)); + + xmlctx = svn_ra_serf__xml_context_create(date_ttable, + NULL, date_closed, NULL, + date_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = report_target; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + handler->body_delegate = create_getdate_body; + handler->body_delegate_baton = date_ctx; + + *date_ctx->revision = SVN_INVALID_REVNUM; + + /* ### use svn_ra_serf__error_on_status() ? */ + + return svn_error_trace(svn_ra_serf__context_run_one(handler, pool)); +} diff --git a/subversion/libsvn_ra_serf/getlocations.c b/subversion/libsvn_ra_serf/getlocations.c new file mode 100644 index 0000000..d3e3175 --- /dev/null +++ b/subversion/libsvn_ra_serf/getlocations.c @@ -0,0 +1,201 @@ +/* + * getlocations.c : entry point for get_locations RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> + +#include <serf.h> + +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_xml.h" +#include "svn_private_config.h" + +#include "../libsvn_ra/ra_loader.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum loc_state_e { + INITIAL = 0, + REPORT, + LOCATION +}; + +typedef struct loc_context_t { + /* pool to allocate memory from */ + apr_pool_t *pool; + + /* parameters set by our caller */ + const char *path; + const apr_array_header_t *location_revisions; + svn_revnum_t peg_revision; + + /* Returned location hash */ + apr_hash_t *paths; + +} loc_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t getloc_ttable[] = { + { INITIAL, S_, "get-locations-report", REPORT, + FALSE, { NULL }, FALSE }, + + { REPORT, S_, "location", LOCATION, + FALSE, { "?rev", "?path", NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +getloc_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + loc_context_t *loc_ctx = baton; + const char *revstr; + const char *path; + + SVN_ERR_ASSERT(leaving_state == LOCATION); + + revstr = svn_hash_gets(attrs, "rev"); + path = svn_hash_gets(attrs, "path"); + if (revstr != NULL && path != NULL) + { + svn_revnum_t rev = SVN_STR_TO_REV(revstr); + apr_hash_set(loc_ctx->paths, + apr_pmemdup(loc_ctx->pool, &rev, sizeof(rev)), sizeof(rev), + apr_pstrdup(loc_ctx->pool, path)); + } + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_get_locations_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + loc_context_t *loc_ctx = baton; + int i; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, + "S:get-locations", + "xmlns:S", SVN_XML_NAMESPACE, + "xmlns:D", "DAV:", + NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "S:path", loc_ctx->path, + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:peg-revision", apr_ltoa(pool, loc_ctx->peg_revision), + alloc); + + for (i = 0; i < loc_ctx->location_revisions->nelts; i++) + { + svn_revnum_t rev = APR_ARRAY_IDX(loc_ctx->location_revisions, i, svn_revnum_t); + svn_ra_serf__add_tag_buckets(buckets, + "S:location-revision", apr_ltoa(pool, rev), + alloc); + } + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, + "S:get-locations"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_locations(svn_ra_session_t *ra_session, + apr_hash_t **locations, + const char *path, + svn_revnum_t peg_revision, + const apr_array_header_t *location_revisions, + apr_pool_t *pool) +{ + loc_context_t *loc_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url; + svn_error_t *err; + + loc_ctx = apr_pcalloc(pool, sizeof(*loc_ctx)); + loc_ctx->pool = pool; + loc_ctx->path = path; + loc_ctx->peg_revision = peg_revision; + loc_ctx->location_revisions = location_revisions; + loc_ctx->paths = apr_hash_make(loc_ctx->pool); + + *locations = loc_ctx->paths; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, peg_revision, + pool, pool)); + + xmlctx = svn_ra_serf__xml_context_create(getloc_ttable, + NULL, getloc_closed, NULL, + loc_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_delegate = create_get_locations_body; + handler->body_delegate_baton = loc_ctx; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + err = svn_ra_serf__context_run_one(handler, pool); + + SVN_ERR(svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + req_url, + handler->location), + err)); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/getlocationsegments.c b/subversion/libsvn_ra_serf/getlocationsegments.c new file mode 100644 index 0000000..d2b69bf --- /dev/null +++ b/subversion/libsvn_ra_serf/getlocationsegments.c @@ -0,0 +1,206 @@ +/* + * getlocationsegments.c : entry point for get_location_segments + * RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_xml.h" +#include "svn_path.h" +#include "svn_private_config.h" +#include "../libsvn_ra/ra_loader.h" + +#include "ra_serf.h" + + + +typedef struct gls_context_t { + /* parameters set by our caller */ + svn_revnum_t peg_revision; + svn_revnum_t start_rev; + svn_revnum_t end_rev; + const char *path; + + /* location segment callback function/baton */ + svn_location_segment_receiver_t receiver; + void *receiver_baton; + +} gls_context_t; + +enum { + INITIAL = 0, + REPORT, + SEGMENT +}; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t gls_ttable[] = { + { INITIAL, S_, "get-location-segments-report", REPORT, + FALSE, { NULL }, FALSE }, + + { REPORT, S_, "location-segment", SEGMENT, + FALSE, { "?path", "range-start", "range-end", NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +gls_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + gls_context_t *gls_ctx = baton; + const char *path; + const char *start_str; + const char *end_str; + svn_location_segment_t segment; + + SVN_ERR_ASSERT(leaving_state == SEGMENT); + + path = svn_hash_gets(attrs, "path"); + start_str = svn_hash_gets(attrs, "range-start"); + end_str = svn_hash_gets(attrs, "range-end"); + + /* The transition table said these must exist. */ + SVN_ERR_ASSERT(start_str && end_str); + + segment.path = path; /* may be NULL */ + segment.range_start = SVN_STR_TO_REV(start_str); + segment.range_end = SVN_STR_TO_REV(end_str); + SVN_ERR(gls_ctx->receiver(&segment, gls_ctx->receiver_baton, scratch_pool)); + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_gls_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + gls_context_t *gls_ctx = baton; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, + "S:get-location-segments", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "S:path", gls_ctx->path, + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:peg-revision", + apr_ltoa(pool, gls_ctx->peg_revision), + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:start-revision", + apr_ltoa(pool, gls_ctx->start_rev), + alloc); + + svn_ra_serf__add_tag_buckets(buckets, + "S:end-revision", + apr_ltoa(pool, gls_ctx->end_rev), + alloc); + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, + "S:get-location-segments"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_location_segments(svn_ra_session_t *ra_session, + const char *path, + svn_revnum_t peg_revision, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_location_segment_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + gls_context_t *gls_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url; + svn_error_t *err; + + gls_ctx = apr_pcalloc(pool, sizeof(*gls_ctx)); + gls_ctx->path = path; + gls_ctx->peg_revision = peg_revision; + gls_ctx->start_rev = start_rev; + gls_ctx->end_rev = end_rev; + gls_ctx->receiver = receiver; + gls_ctx->receiver_baton = receiver_baton; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, peg_revision, + pool, pool)); + + xmlctx = svn_ra_serf__xml_context_create(gls_ttable, + NULL, gls_closed, NULL, + gls_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_delegate = create_gls_body; + handler->body_delegate_baton = gls_ctx; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + err = svn_ra_serf__context_run_one(handler, pool); + + err = svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location), + err); + + if (err && (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)) + return svn_error_create(SVN_ERR_RA_NOT_IMPLEMENTED, err, NULL); + + return svn_error_trace(err); +} diff --git a/subversion/libsvn_ra_serf/getlocks.c b/subversion/libsvn_ra_serf/getlocks.c new file mode 100644 index 0000000..61b8b8c --- /dev/null +++ b/subversion/libsvn_ra_serf/getlocks.c @@ -0,0 +1,277 @@ +/* + * getlocks.c : entry point for get_locks RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> + +#include <serf.h> + +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_time.h" +#include "svn_xml.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_fspath.h" +#include "svn_private_config.h" + +#include "../libsvn_ra/ra_loader.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum { + INITIAL = 0, + REPORT, + LOCK, + PATH, + TOKEN, + OWNER, + COMMENT, + CREATION_DATE, + EXPIRATION_DATE +}; + +typedef struct lock_context_t { + apr_pool_t *pool; + + /* target and requested depth of the operation. */ + const char *path; + svn_depth_t requested_depth; + + /* return hash */ + apr_hash_t *hash; + +} lock_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t getlocks_ttable[] = { + { INITIAL, S_, "get-locks-report", REPORT, + FALSE, { NULL }, FALSE }, + + { REPORT, S_, "lock", LOCK, + FALSE, { NULL }, TRUE }, + + { LOCK, S_, "path", PATH, + TRUE, { NULL }, TRUE }, + + { LOCK, S_, "token", TOKEN, + TRUE, { NULL }, TRUE }, + + { LOCK, S_, "owner", OWNER, + TRUE, { NULL }, TRUE }, + + { LOCK, S_, "comment", COMMENT, + TRUE, { NULL }, TRUE }, + + { LOCK, S_, SVN_DAV__CREATIONDATE, CREATION_DATE, + TRUE, { NULL }, TRUE }, + + { LOCK, S_, "expirationdate", EXPIRATION_DATE, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +getlocks_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + lock_context_t *lock_ctx = baton; + + if (leaving_state == LOCK) + { + const char *path = svn_hash_gets(attrs, "path"); + svn_boolean_t save_lock = FALSE; + + /* Filter out unwanted paths. Since Subversion only allows + locks on files, we can treat depth=immediates the same as + depth=files for filtering purposes. Meaning, we'll keep + this lock if: + + a) its path is the very path we queried, or + b) we've asked for a fully recursive answer, or + c) we've asked for depth=files or depth=immediates, and this + lock is on an immediate child of our query path. + */ + if (strcmp(lock_ctx->path, path) == 0 + || lock_ctx->requested_depth == svn_depth_infinity) + { + save_lock = TRUE; + } + else if (lock_ctx->requested_depth == svn_depth_files + || lock_ctx->requested_depth == svn_depth_immediates) + { + const char *relpath = svn_fspath__skip_ancestor(lock_ctx->path, + path); + if (relpath && (svn_path_component_count(relpath) == 1)) + save_lock = TRUE; + } + + if (save_lock) + { + /* We get to put the structure on the stack rather than using + svn_lock_create(). Bwahahaha.... */ + svn_lock_t lock = { 0 }; + const char *date; + svn_lock_t *result_lock; + + /* Note: these "attributes" came from child elements. Some of + them may have not been sent, so the value will be NULL. */ + + lock.path = path; + lock.token = svn_hash_gets(attrs, "token"); + lock.owner = svn_hash_gets(attrs, "owner"); + lock.comment = svn_hash_gets(attrs, "comment"); + + date = svn_hash_gets(attrs, SVN_DAV__CREATIONDATE); + if (date) + SVN_ERR(svn_time_from_cstring(&lock.creation_date, date, + scratch_pool)); + + date = svn_hash_gets(attrs, "expirationdate"); + if (date) + SVN_ERR(svn_time_from_cstring(&lock.expiration_date, date, + scratch_pool)); + + result_lock = svn_lock_dup(&lock, lock_ctx->pool); + svn_hash_sets(lock_ctx->hash, result_lock->path, result_lock); + } + } + else + { + const char *name; + + SVN_ERR_ASSERT(cdata != NULL); + + if (leaving_state == PATH) + name = "path"; + else if (leaving_state == TOKEN) + name = "token"; + else if (leaving_state == OWNER) + name = "owner"; + else if (leaving_state == COMMENT) + name = "comment"; + else if (leaving_state == CREATION_DATE) + name = SVN_DAV__CREATIONDATE; + else if (leaving_state == EXPIRATION_DATE) + name = "expirationdate"; + else + SVN_ERR_MALFUNCTION(); + + /* Store the lock information onto the LOCK elemstate. */ + svn_ra_serf__xml_note(xes, LOCK, name, cdata->data); + } + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_getlocks_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + lock_context_t *lock_ctx = baton; + serf_bucket_t *buckets; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets( + buckets, alloc, "S:get-locks-report", "xmlns:S", SVN_XML_NAMESPACE, + "depth", svn_depth_to_word(lock_ctx->requested_depth), NULL); + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "S:get-locks-report"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_locks(svn_ra_session_t *ra_session, + apr_hash_t **locks, + const char *path, + svn_depth_t depth, + apr_pool_t *pool) +{ + lock_context_t *lock_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url, *rel_path; + + req_url = svn_path_url_add_component2(session->session_url.path, path, pool); + SVN_ERR(svn_ra_serf__get_relative_path(&rel_path, req_url, session, + NULL, pool)); + + lock_ctx = apr_pcalloc(pool, sizeof(*lock_ctx)); + lock_ctx->pool = pool; + lock_ctx->path = apr_pstrcat(pool, "/", rel_path, (char *)NULL); + lock_ctx->requested_depth = depth; + lock_ctx->hash = apr_hash_make(pool); + + xmlctx = svn_ra_serf__xml_context_create(getlocks_ttable, + NULL, getlocks_closed, NULL, + lock_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + handler->body_delegate = create_getlocks_body; + handler->body_delegate_baton = lock_ctx; + + SVN_ERR(svn_ra_serf__context_run_one(handler, pool)); + + /* We get a 404 when a path doesn't exist in HEAD, but it might + have existed earlier (E.g. 'svn ls http://s/svn/trunk/file@1' */ + if (handler->sline.code != 404) + { + SVN_ERR(svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location)); + } + + *locks = lock_ctx->hash; + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/inherited_props.c b/subversion/libsvn_ra_serf/inherited_props.c new file mode 100644 index 0000000..9283a5a --- /dev/null +++ b/subversion/libsvn_ra_serf/inherited_props.c @@ -0,0 +1,344 @@ +/* + * inherited_props.c : ra_serf implementation of svn_ra_get_inherited_props + * + * ==================================================================== + * 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_tables.h> +#include <apr_xml.h> + +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_ra.h" +#include "svn_string.h" +#include "svn_xml.h" +#include "svn_props.h" +#include "svn_base64.h" + +#include "private/svn_dav_protocol.h" +#include "../libsvn_ra/ra_loader.h" +#include "svn_private_config.h" +#include "ra_serf.h" + + +/* The current state of our XML parsing. */ +typedef enum iprops_state_e { + NONE = 0, + IPROPS_REPORT, + IPROPS_ITEM, + IPROPS_PATH, + IPROPS_PROPNAME, + IPROPS_PROPVAL +} iprops_state_e; + +/* Struct for accumulating inherited props. */ +typedef struct iprops_context_t { + /* The depth-first ordered array of svn_prop_inherited_item_t * + structures we are building. */ + apr_array_header_t *iprops; + + /* Pool in which to allocate elements of IPROPS. */ + apr_pool_t *pool; + + /* The repository's root URL. */ + const char *repos_root_url; + + /* Current CDATA values*/ + svn_stringbuf_t *curr_path; + svn_stringbuf_t *curr_propname; + svn_stringbuf_t *curr_propval; + const char *curr_prop_val_encoding; + + /* Current element in IPROPS. */ + svn_prop_inherited_item_t *curr_iprop; + + /* Serf context completion flag for svn_ra_serf__context_run_wait() */ + svn_boolean_t done; + + /* Path we are finding inherited properties for. This is relative to + the RA session passed to svn_ra_serf__get_inherited_props. */ + const char *path; + /* The revision of PATH*/ + svn_revnum_t revision; +} iprops_context_t; + +static svn_error_t * +start_element(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool) +{ + iprops_context_t *iprops_ctx = parser->user_data; + iprops_state_e state; + + state = parser->state->current_state; + if (state == NONE + && strcmp(name.name, SVN_DAV__INHERITED_PROPS_REPORT) == 0) + { + svn_ra_serf__xml_push_state(parser, IPROPS_REPORT); + } + else if (state == IPROPS_REPORT && + strcmp(name.name, SVN_DAV__IPROP_ITEM) == 0) + { + svn_stringbuf_setempty(iprops_ctx->curr_path); + svn_stringbuf_setempty(iprops_ctx->curr_propname); + svn_stringbuf_setempty(iprops_ctx->curr_propval); + iprops_ctx->curr_prop_val_encoding = NULL; + iprops_ctx->curr_iprop = NULL; + svn_ra_serf__xml_push_state(parser, IPROPS_ITEM); + } + else if (state == IPROPS_ITEM && + strcmp(name.name, SVN_DAV__IPROP_PROPVAL) == 0) + { + const char *prop_val_encoding = svn_xml_get_attr_value("encoding", + attrs); + iprops_ctx->curr_prop_val_encoding = apr_pstrdup(iprops_ctx->pool, + prop_val_encoding); + svn_ra_serf__xml_push_state(parser, IPROPS_PROPVAL); + } + else if (state == IPROPS_ITEM && + strcmp(name.name, SVN_DAV__IPROP_PATH) == 0) + { + svn_ra_serf__xml_push_state(parser, IPROPS_PATH); + } + else if (state == IPROPS_ITEM && + strcmp(name.name, SVN_DAV__IPROP_PROPNAME) == 0) + { + svn_ra_serf__xml_push_state(parser, IPROPS_PROPNAME); + } + else if (state == IPROPS_ITEM && + strcmp(name.name, SVN_DAV__IPROP_PROPVAL) == 0) + { + svn_ra_serf__xml_push_state(parser, IPROPS_PROPVAL); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +end_element(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool) +{ + iprops_context_t *iprops_ctx = parser->user_data; + iprops_state_e state; + + state = parser->state->current_state; + + if (state == IPROPS_REPORT && + strcmp(name.name, SVN_DAV__INHERITED_PROPS_REPORT) == 0) + { + svn_ra_serf__xml_pop_state(parser); + } + else if (state == IPROPS_PATH + && strcmp(name.name, SVN_DAV__IPROP_PATH) == 0) + { + iprops_ctx->curr_iprop = apr_palloc( + iprops_ctx->pool, sizeof(svn_prop_inherited_item_t)); + + iprops_ctx->curr_iprop->path_or_url = + svn_path_url_add_component2(iprops_ctx->repos_root_url, + iprops_ctx->curr_path->data, + iprops_ctx->pool); + iprops_ctx->curr_iprop->prop_hash = apr_hash_make(iprops_ctx->pool); + svn_ra_serf__xml_pop_state(parser); + } + else if (state == IPROPS_PROPVAL + && strcmp(name.name, SVN_DAV__IPROP_PROPVAL) == 0) + { + const svn_string_t *prop_val; + + if (iprops_ctx->curr_prop_val_encoding) + { + svn_string_t encoded_prop_val; + + if (strcmp(iprops_ctx->curr_prop_val_encoding, "base64") != 0) + return svn_error_create(SVN_ERR_XML_MALFORMED, NULL, NULL); + + encoded_prop_val.data = iprops_ctx->curr_propval->data; + encoded_prop_val.len = iprops_ctx->curr_propval->len; + prop_val = svn_base64_decode_string(&encoded_prop_val, + iprops_ctx->pool); + } + else + { + prop_val = svn_string_create_from_buf(iprops_ctx->curr_propval, + iprops_ctx->pool); + } + + svn_hash_sets(iprops_ctx->curr_iprop->prop_hash, + apr_pstrdup(iprops_ctx->pool, + iprops_ctx->curr_propname->data), + prop_val); + /* Clear current propname and propval in the event there are + multiple properties on the current path. */ + svn_stringbuf_setempty(iprops_ctx->curr_propname); + svn_stringbuf_setempty(iprops_ctx->curr_propval); + svn_ra_serf__xml_pop_state(parser); + } + else if (state == IPROPS_PROPNAME + && strcmp(name.name, SVN_DAV__IPROP_PROPNAME) == 0) + { + svn_ra_serf__xml_pop_state(parser); + } + else if (state == IPROPS_ITEM + && strcmp(name.name, SVN_DAV__IPROP_ITEM) == 0) + { + APR_ARRAY_PUSH(iprops_ctx->iprops, svn_prop_inherited_item_t *) = + iprops_ctx->curr_iprop; + svn_ra_serf__xml_pop_state(parser); + } + return SVN_NO_ERROR; +} + + +static svn_error_t * +cdata_handler(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + iprops_context_t *iprops_ctx = parser->user_data; + iprops_state_e state = parser->state->current_state; + + switch (state) + { + case IPROPS_PATH: + svn_stringbuf_appendbytes(iprops_ctx->curr_path, data, len); + break; + + case IPROPS_PROPNAME: + svn_stringbuf_appendbytes(iprops_ctx->curr_propname, data, len); + break; + + case IPROPS_PROPVAL: + svn_stringbuf_appendbytes(iprops_ctx->curr_propval, data, len); + break; + + default: + break; + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +create_iprops_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + iprops_context_t *iprops_ctx = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "S:" SVN_DAV__INHERITED_PROPS_REPORT, + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + svn_ra_serf__add_tag_buckets(body_bkt, + "S:" SVN_DAV__REVISION, + apr_ltoa(pool, iprops_ctx->revision), + alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "S:" SVN_DAV__PATH, + iprops_ctx->path, alloc); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, + "S:" SVN_DAV__INHERITED_PROPS_REPORT); + *bkt = body_bkt; + return SVN_NO_ERROR; +} + +/* Request a inherited-props-report from the URL attached to RA_SESSION, + and fill the IPROPS array hash with the results. */ +svn_error_t * +svn_ra_serf__get_inherited_props(svn_ra_session_t *ra_session, + apr_array_header_t **iprops, + const char *path, + svn_revnum_t revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + iprops_context_t *iprops_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_parser_t *parser_ctx; + const char *req_url; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, + NULL /* latest_revnum */, + session, + NULL /* conn */, + NULL /* url */, + revision, + result_pool, scratch_pool)); + + SVN_ERR_ASSERT(session->repos_root_str); + + iprops_ctx = apr_pcalloc(scratch_pool, sizeof(*iprops_ctx)); + iprops_ctx->done = FALSE; + iprops_ctx->repos_root_url = session->repos_root_str; + iprops_ctx->pool = result_pool; + iprops_ctx->curr_path = svn_stringbuf_create_empty(scratch_pool); + iprops_ctx->curr_propname = svn_stringbuf_create_empty(scratch_pool); + iprops_ctx->curr_propval = svn_stringbuf_create_empty(scratch_pool); + iprops_ctx->curr_iprop = NULL; + iprops_ctx->iprops = apr_array_make(result_pool, 1, + sizeof(svn_prop_inherited_item_t *)); + iprops_ctx->path = path; + iprops_ctx->revision = revision; + + handler = apr_pcalloc(scratch_pool, sizeof(*handler)); + + handler->method = "REPORT"; + handler->path = req_url; + handler->conn = session->conns[0]; + handler->session = session; + handler->body_delegate = create_iprops_body; + handler->body_delegate_baton = iprops_ctx; + handler->body_type = "text/xml"; + handler->handler_pool = scratch_pool; + + parser_ctx = apr_pcalloc(scratch_pool, sizeof(*parser_ctx)); + + parser_ctx->pool = scratch_pool; + parser_ctx->user_data = iprops_ctx; + parser_ctx->start = start_element; + parser_ctx->end = end_element; + parser_ctx->cdata = cdata_handler; + parser_ctx->done = &iprops_ctx->done; + + handler->response_handler = svn_ra_serf__handle_xml_parser; + handler->response_baton = parser_ctx; + + err = svn_ra_serf__context_run_one(handler, scratch_pool); + SVN_ERR(svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location), + err)); + + if (iprops_ctx->done) + *iprops = iprops_ctx->iprops; + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/locks.c b/subversion/libsvn_ra_serf/locks.c new file mode 100644 index 0000000..db2d371 --- /dev/null +++ b/subversion/libsvn_ra_serf/locks.c @@ -0,0 +1,654 @@ +/* + * locks.c : entry point for locking RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_dav.h" +#include "svn_pools.h" +#include "svn_ra.h" + +#include "../libsvn_ra/ra_loader.h" +#include "svn_config.h" +#include "svn_path.h" +#include "svn_time.h" +#include "svn_private_config.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum { + INITIAL = 0, + MULTISTATUS, + RESPONSE, + PROPSTAT, + PROP, + LOCK_DISCOVERY, + ACTIVE_LOCK, + LOCK_TYPE, + LOCK_SCOPE, + DEPTH, + TIMEOUT, + LOCK_TOKEN, + OWNER, + HREF +}; + +typedef struct lock_info_t { + apr_pool_t *pool; + + const char *path; + + svn_lock_t *lock; + + svn_boolean_t force; + svn_revnum_t revision; + + svn_boolean_t read_headers; + + svn_ra_serf__handler_t *handler; + + /* The expat handler. We wrap this to do a bit more work. */ + svn_ra_serf__response_handler_t inner_handler; + void *inner_baton; + +} lock_info_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t locks_ttable[] = { + /* The INITIAL state can transition into D:prop (LOCK) or + to D:multistatus (PROPFIND) */ + { INITIAL, D_, "prop", PROP, + FALSE, { NULL }, FALSE }, + { INITIAL, D_, "multistatus", MULTISTATUS, + FALSE, { NULL }, FALSE }, + + { MULTISTATUS, D_, "response", RESPONSE, + FALSE, { NULL }, FALSE }, + + { RESPONSE, D_, "propstat", PROPSTAT, + FALSE, { NULL }, FALSE }, + + { PROPSTAT, D_, "prop", PROP, + FALSE, { NULL }, FALSE }, + + { PROP, D_, "lockdiscovery", LOCK_DISCOVERY, + FALSE, { NULL }, FALSE }, + + { LOCK_DISCOVERY, D_, "activelock", ACTIVE_LOCK, + FALSE, { NULL }, FALSE }, + +#if 0 + /* ### we don't really need to parse locktype/lockscope. we know what + ### the values are going to be. we *could* validate that the only + ### possible children are D:write and D:exclusive. we'd need to + ### modify the state transition to tell us about all children + ### (ie. maybe support "*" for the name) and then validate. but it + ### just isn't important to validate, so disable this for now... */ + + { ACTIVE_LOCK, D_, "locktype", LOCK_TYPE, + FALSE, { NULL }, FALSE }, + + { LOCK_TYPE, D_, "write", WRITE, + FALSE, { NULL }, TRUE }, + + { ACTIVE_LOCK, D_, "lockscope", LOCK_SCOPE, + FALSE, { NULL }, FALSE }, + + { LOCK_SCOPE, D_, "exclusive", EXCLUSIVE, + FALSE, { NULL }, TRUE }, +#endif /* 0 */ + + { ACTIVE_LOCK, D_, "timeout", TIMEOUT, + TRUE, { NULL }, TRUE }, + + { ACTIVE_LOCK, D_, "locktoken", LOCK_TOKEN, + FALSE, { NULL }, FALSE }, + + { LOCK_TOKEN, D_, "href", HREF, + TRUE, { NULL }, TRUE }, + + { ACTIVE_LOCK, D_, "owner", OWNER, + TRUE, { NULL }, TRUE }, + + /* ACTIVE_LOCK has a D:depth child, but we can ignore that. */ + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +locks_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + lock_info_t *lock_ctx = baton; + + if (leaving_state == TIMEOUT) + { + if (strcmp(cdata->data, "Infinite") == 0) + lock_ctx->lock->expiration_date = 0; + else + SVN_ERR(svn_time_from_cstring(&lock_ctx->lock->creation_date, + cdata->data, lock_ctx->pool)); + } + else if (leaving_state == HREF) + { + if (cdata->len) + { + char *buf = apr_pstrmemdup(lock_ctx->pool, cdata->data, cdata->len); + + apr_collapse_spaces(buf, buf); + lock_ctx->lock->token = buf; + } + } + else if (leaving_state == OWNER) + { + if (cdata->len) + { + lock_ctx->lock->comment = apr_pstrmemdup(lock_ctx->pool, + cdata->data, cdata->len); + } + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +set_lock_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + lock_info_t *lock_ctx = baton; + + if (lock_ctx->force) + { + serf_bucket_headers_set(headers, SVN_DAV_OPTIONS_HEADER, + SVN_DAV_OPTION_LOCK_STEAL); + } + + if (SVN_IS_VALID_REVNUM(lock_ctx->revision)) + { + serf_bucket_headers_set(headers, SVN_DAV_VERSION_NAME_HEADER, + apr_ltoa(pool, lock_ctx->revision)); + } + + return APR_SUCCESS; +} + + +/* Register an error within the session. If something is already there, + then it will take precedence. */ +static svn_error_t * +determine_error(svn_ra_serf__handler_t *handler, + svn_error_t *err) +{ + { + apr_status_t errcode; + + if (handler->sline.code == 423) + errcode = SVN_ERR_FS_PATH_ALREADY_LOCKED; + else if (handler->sline.code == 403) + errcode = SVN_ERR_RA_DAV_FORBIDDEN; + else + return err; + + /* Client-side or server-side error already. Return it. */ + if (err != NULL) + return err; + + /* The server did not send us a detailed human-readable error. + Provide a generic error. */ + err = svn_error_createf(errcode, NULL, + _("Lock request failed: %d %s"), + handler->sline.code, + handler->sline.reason); + } + + return err; +} + + +/* Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +handle_lock(serf_request_t *request, + serf_bucket_t *response, + void *handler_baton, + apr_pool_t *pool) +{ + lock_info_t *ctx = handler_baton; + + /* 403 (Forbidden) when a lock doesn't exist. + 423 (Locked) when a lock already exists. */ + if (ctx->handler->sline.code == 403 + || ctx->handler->sline.code == 423) + { + /* Go look in the body for a server-provided error. This will + reset flags for the core handler to Do The Right Thing. We + won't be back to this handler again. */ + return svn_error_trace(svn_ra_serf__expect_empty_body( + request, response, ctx->handler, pool)); + } + + if (!ctx->read_headers) + { + serf_bucket_t *headers; + const char *val; + + headers = serf_bucket_response_get_headers(response); + + val = serf_bucket_headers_get(headers, SVN_DAV_LOCK_OWNER_HEADER); + if (val) + { + ctx->lock->owner = apr_pstrdup(ctx->pool, val); + } + + val = serf_bucket_headers_get(headers, SVN_DAV_CREATIONDATE_HEADER); + if (val) + { + SVN_ERR(svn_time_from_cstring(&ctx->lock->creation_date, val, + ctx->pool)); + } + + ctx->read_headers = TRUE; + } + + return ctx->inner_handler(request, response, ctx->inner_baton, pool); +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_getlock_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(buckets, alloc); + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "propfind", + "xmlns", "DAV:", + NULL); + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "prop", NULL); + svn_ra_serf__add_tag_buckets(buckets, "lockdiscovery", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "prop"); + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "propfind"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +static svn_error_t* +setup_getlock_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + serf_bucket_headers_setn(headers, "Depth", "0"); + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__request_body_delegate_t */ +static svn_error_t * +create_lock_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + lock_info_t *ctx = baton; + serf_bucket_t *buckets; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(buckets, alloc); + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "lockinfo", + "xmlns", "DAV:", + NULL); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "lockscope", NULL); + svn_ra_serf__add_tag_buckets(buckets, "exclusive", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "lockscope"); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, "locktype", NULL); + svn_ra_serf__add_tag_buckets(buckets, "write", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "locktype"); + + if (ctx->lock->comment) + { + svn_ra_serf__add_tag_buckets(buckets, "owner", ctx->lock->comment, + alloc); + } + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, "lockinfo"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_lock(svn_ra_session_t *ra_session, + svn_lock_t **lock, + const char *path, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + lock_info_t *lock_ctx; + const char *req_url; + svn_error_t *err; + + req_url = svn_path_url_add_component2(session->session_url.path, path, pool); + + lock_ctx = apr_pcalloc(pool, sizeof(*lock_ctx)); + + lock_ctx->pool = pool; + lock_ctx->path = req_url; + lock_ctx->lock = svn_lock_create(pool); + lock_ctx->lock->path = apr_pstrdup(pool, path); /* be sure */ + + xmlctx = svn_ra_serf__xml_context_create(locks_ttable, + NULL, locks_closed, NULL, + lock_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "PROPFIND"; + handler->path = req_url; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + handler->body_delegate = create_getlock_body; + handler->body_delegate_baton = lock_ctx; + + handler->header_delegate = setup_getlock_headers; + handler->header_delegate_baton = lock_ctx; + + lock_ctx->inner_handler = handler->response_handler; + lock_ctx->inner_baton = handler->response_baton; + handler->response_handler = handle_lock; + handler->response_baton = lock_ctx; + + lock_ctx->handler = handler; + + err = svn_ra_serf__context_run_one(handler, pool); + err = determine_error(handler, err); + + if (handler->sline.code == 404) + { + return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, err, + _("Malformed URL for repository")); + } + if (err) + { + /* TODO Shh. We're telling a white lie for now. */ + return svn_error_create(SVN_ERR_RA_NOT_IMPLEMENTED, err, + _("Server does not support locking features")); + } + + *lock = lock_ctx->lock; + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__lock(svn_ra_session_t *ra_session, + apr_hash_t *path_revs, + const char *comment, + svn_boolean_t force, + svn_ra_lock_callback_t lock_func, + void *lock_baton, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + + /* ### TODO for issue 2263: Send all the locks over the wire at once. This + ### loop is just a temporary shim. + ### an alternative, which is backwards-compat with all servers is to + ### pipeline these requests. ie. stop using run_wait/run_one. */ + + for (hi = apr_hash_first(scratch_pool, path_revs); + hi; + hi = apr_hash_next(hi)) + { + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *req_url; + lock_info_t *lock_ctx; + svn_error_t *err; + svn_error_t *new_err = NULL; + + svn_pool_clear(iterpool); + + lock_ctx = apr_pcalloc(iterpool, sizeof(*lock_ctx)); + + lock_ctx->pool = iterpool; + lock_ctx->path = svn__apr_hash_index_key(hi); + lock_ctx->revision = *((svn_revnum_t*)svn__apr_hash_index_val(hi)); + lock_ctx->lock = svn_lock_create(iterpool); + lock_ctx->lock->path = lock_ctx->path; + lock_ctx->lock->comment = comment; + + lock_ctx->force = force; + req_url = svn_path_url_add_component2(session->session_url.path, + lock_ctx->path, iterpool); + + xmlctx = svn_ra_serf__xml_context_create(locks_ttable, + NULL, locks_closed, NULL, + lock_ctx, + iterpool); + handler = svn_ra_serf__create_expat_handler(xmlctx, iterpool); + + handler->method = "LOCK"; + handler->path = req_url; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + handler->header_delegate = set_lock_headers; + handler->header_delegate_baton = lock_ctx; + + handler->body_delegate = create_lock_body; + handler->body_delegate_baton = lock_ctx; + + lock_ctx->inner_handler = handler->response_handler; + lock_ctx->inner_baton = handler->response_baton; + handler->response_handler = handle_lock; + handler->response_baton = lock_ctx; + + lock_ctx->handler = handler; + + err = svn_ra_serf__context_run_one(handler, iterpool); + err = determine_error(handler, err); + + if (lock_func) + new_err = lock_func(lock_baton, lock_ctx->path, TRUE, lock_ctx->lock, + err, iterpool); + svn_error_clear(err); + + SVN_ERR(new_err); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +struct unlock_context_t { + const char *token; + svn_boolean_t force; +}; + +static svn_error_t * +set_unlock_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + struct unlock_context_t *ctx = baton; + + serf_bucket_headers_set(headers, "Lock-Token", ctx->token); + if (ctx->force) + { + serf_bucket_headers_set(headers, SVN_DAV_OPTIONS_HEADER, + SVN_DAV_OPTION_LOCK_BREAK); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__unlock(svn_ra_session_t *ra_session, + apr_hash_t *path_tokens, + svn_boolean_t force, + svn_ra_lock_callback_t lock_func, + void *lock_baton, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_hash_index_t *hi; + apr_pool_t *iterpool; + + iterpool = svn_pool_create(scratch_pool); + + /* ### TODO for issue 2263: Send all the locks over the wire at once. This + ### loop is just a temporary shim. + ### an alternative, which is backwards-compat with all servers is to + ### pipeline these requests. ie. stop using run_wait/run_one. */ + + for (hi = apr_hash_first(scratch_pool, path_tokens); + hi; + hi = apr_hash_next(hi)) + { + svn_ra_serf__handler_t *handler; + const char *req_url, *path, *token; + svn_lock_t *existing_lock = NULL; + struct unlock_context_t unlock_ctx; + svn_error_t *err = NULL; + svn_error_t *new_err = NULL; + + + svn_pool_clear(iterpool); + + path = svn__apr_hash_index_key(hi); + token = svn__apr_hash_index_val(hi); + + if (force && (!token || token[0] == '\0')) + { + SVN_ERR(svn_ra_serf__get_lock(ra_session, &existing_lock, path, + iterpool)); + token = existing_lock->token; + if (!token) + { + err = svn_error_createf(SVN_ERR_RA_NOT_LOCKED, NULL, + _("'%s' is not locked in the repository"), + path); + + if (lock_func) + { + svn_error_t *err2; + err2 = lock_func(lock_baton, path, FALSE, NULL, err, + iterpool); + svn_error_clear(err); + err = NULL; + if (err2) + return svn_error_trace(err2); + } + else + { + svn_error_clear(err); + err = NULL; + } + continue; + } + } + + unlock_ctx.force = force; + unlock_ctx.token = apr_pstrcat(iterpool, "<", token, ">", (char *)NULL); + + req_url = svn_path_url_add_component2(session->session_url.path, path, + iterpool); + + handler = apr_pcalloc(iterpool, sizeof(*handler)); + + handler->handler_pool = iterpool; + handler->method = "UNLOCK"; + handler->path = req_url; + handler->conn = session->conns[0]; + handler->session = session; + + handler->header_delegate = set_unlock_headers; + handler->header_delegate_baton = &unlock_ctx; + + handler->response_handler = svn_ra_serf__expect_empty_body; + handler->response_baton = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, iterpool)); + + switch (handler->sline.code) + { + case 204: + break; /* OK */ + case 403: + /* Api users expect this specific error code to detect failures */ + err = svn_error_createf(SVN_ERR_FS_LOCK_OWNER_MISMATCH, NULL, + _("Unlock request failed: %d %s"), + handler->sline.code, + handler->sline.reason); + break; + default: + err = svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("Unlock request failed: %d %s"), + handler->sline.code, + handler->sline.reason); + } + + if (lock_func) + new_err = lock_func(lock_baton, path, FALSE, existing_lock, err, + iterpool); + + svn_error_clear(err); + SVN_ERR(new_err); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/log.c b/subversion/libsvn_ra_serf/log.c new file mode 100644 index 0000000..e44753b --- /dev/null +++ b/subversion/libsvn_ra_serf/log.c @@ -0,0 +1,604 @@ +/* + * log.c : entry point for log RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_base64.h" +#include "svn_xml.h" +#include "svn_config.h" +#include "svn_path.h" +#include "svn_props.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_string_private.h" +#include "private/svn_subr_private.h" +#include "svn_private_config.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + */ +enum { + INITIAL = 0, + REPORT, + ITEM, + VERSION, + CREATOR, + DATE, + COMMENT, + REVPROP, + HAS_CHILDREN, + ADDED_PATH, + REPLACED_PATH, + DELETED_PATH, + MODIFIED_PATH, + SUBTRACTIVE_MERGE +}; + +typedef struct log_context_t { + apr_pool_t *pool; + + /* parameters set by our caller */ + const apr_array_header_t *paths; + svn_revnum_t start; + svn_revnum_t end; + int limit; + svn_boolean_t changed_paths; + svn_boolean_t strict_node_history; + svn_boolean_t include_merged_revisions; + const apr_array_header_t *revprops; + int nest_level; /* used to track mergeinfo nesting levels */ + int count; /* only incremented when nest_level == 0 */ + + /* Collect information for storage into a log entry. Most of the entry + members are collected by individual states. revprops and paths are + N datapoints per entry. */ + apr_hash_t *collect_revprops; + apr_hash_t *collect_paths; + + /* log receiver function and baton */ + svn_log_entry_receiver_t receiver; + void *receiver_baton; + + /* pre-1.5 compatibility */ + svn_boolean_t want_author; + svn_boolean_t want_date; + svn_boolean_t want_message; +} log_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t log_ttable[] = { + { INITIAL, S_, "log-report", REPORT, + FALSE, { NULL }, FALSE }, + + /* Note that we have an opener here. We need to construct a new LOG_ENTRY + to record multiple paths. */ + { REPORT, S_, "log-item", ITEM, + FALSE, { NULL }, TRUE }, + + { ITEM, D_, SVN_DAV__VERSION_NAME, VERSION, + TRUE, { NULL }, TRUE }, + + { ITEM, D_, "creator-displayname", CREATOR, + TRUE, { "?encoding", NULL }, TRUE }, + + { ITEM, S_, "date", DATE, + TRUE, { "?encoding", NULL }, TRUE }, + + { ITEM, D_, "comment", COMMENT, + TRUE, { "?encoding", NULL }, TRUE }, + + { ITEM, S_, "revprop", REVPROP, + TRUE, { "name", "?encoding", NULL }, TRUE }, + + { ITEM, S_, "has-children", HAS_CHILDREN, + FALSE, { NULL }, TRUE }, + + { ITEM, S_, "subtractive-merge", SUBTRACTIVE_MERGE, + FALSE, { NULL }, TRUE }, + + { ITEM, S_, "added-path", ADDED_PATH, + TRUE, { "?node-kind", "?text-mods", "?prop-mods", + "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { ITEM, S_, "replaced-path", REPLACED_PATH, + TRUE, { "?node-kind", "?text-mods", "?prop-mods", + "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { ITEM, S_, "deleted-path", DELETED_PATH, + TRUE, { "?node-kind", "?text-mods", "?prop-mods", NULL }, TRUE }, + + { ITEM, S_, "modified-path", MODIFIED_PATH, + TRUE, { "?node-kind", "?text-mods", "?prop-mods", NULL }, TRUE }, + + { 0 } +}; + + +/* Store CDATA into REVPROPS, associated with PROPNAME. If ENCODING is not + NULL, then it must base "base64" and CDATA will be decoded first. + + NOTE: PROPNAME must live longer than REVPROPS. */ +static svn_error_t * +collect_revprop(apr_hash_t *revprops, + const char *propname, + const svn_string_t *cdata, + const char *encoding) +{ + apr_pool_t *result_pool = apr_hash_pool_get(revprops); + const svn_string_t *decoded; + + if (encoding) + { + /* Check for a known encoding type. This is easy -- there's + only one. */ + if (strcmp(encoding, "base64") != 0) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unsupported encoding '%s'"), + encoding); + } + + decoded = svn_base64_decode_string(cdata, result_pool); + } + else + { + decoded = svn_string_dup(cdata, result_pool); + } + + /* Caller has ensured PROPNAME has sufficient lifetime. */ + svn_hash_sets(revprops, propname, decoded); + + return SVN_NO_ERROR; +} + + +/* Record ACTION on the path in CDATA into PATHS. Other properties about + the action are pulled from ATTRS. */ +static svn_error_t * +collect_path(apr_hash_t *paths, + char action, + const svn_string_t *cdata, + apr_hash_t *attrs) +{ + apr_pool_t *result_pool = apr_hash_pool_get(paths); + svn_log_changed_path2_t *lcp; + const char *copyfrom_path; + const char *copyfrom_rev; + const char *path; + + lcp = svn_log_changed_path2_create(result_pool); + lcp->action = action; + lcp->copyfrom_rev = SVN_INVALID_REVNUM; + + /* COPYFROM_* are only recorded for ADDED_PATH and REPLACED_PATH. */ + copyfrom_path = svn_hash_gets(attrs, "copyfrom-path"); + copyfrom_rev = svn_hash_gets(attrs, "copyfrom-rev"); + if (copyfrom_path && copyfrom_rev) + { + svn_revnum_t rev = SVN_STR_TO_REV(copyfrom_rev); + + if (SVN_IS_VALID_REVNUM(rev)) + { + lcp->copyfrom_path = apr_pstrdup(result_pool, copyfrom_path); + lcp->copyfrom_rev = rev; + } + } + + lcp->node_kind = svn_node_kind_from_word(svn_hash_gets(attrs, "node-kind")); + lcp->text_modified = svn_tristate__from_word(svn_hash_gets(attrs, + "text-mods")); + lcp->props_modified = svn_tristate__from_word(svn_hash_gets(attrs, + "prop-mods")); + + path = apr_pstrmemdup(result_pool, cdata->data, cdata->len); + svn_hash_sets(paths, path, lcp); + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_opened_t */ +static svn_error_t * +log_opened(svn_ra_serf__xml_estate_t *xes, + void *baton, + int entered_state, + const svn_ra_serf__dav_props_t *tag, + apr_pool_t *scratch_pool) +{ + log_context_t *log_ctx = baton; + + if (entered_state == ITEM) + { + apr_pool_t *state_pool = svn_ra_serf__xml_state_pool(xes); + + log_ctx->collect_revprops = apr_hash_make(state_pool); + log_ctx->collect_paths = apr_hash_make(state_pool); + } + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +log_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + log_context_t *log_ctx = baton; + + if (leaving_state == ITEM) + { + svn_log_entry_t *log_entry; + const char *rev_str; + + if (log_ctx->limit && (log_ctx->nest_level == 0) + && (++log_ctx->count > log_ctx->limit)) + { + return SVN_NO_ERROR; + } + + log_entry = svn_log_entry_create(scratch_pool); + + /* Pick up the paths from the context. These have the same lifetime + as this state. That is long enough for us to pass the paths to + the receiver callback. */ + if (apr_hash_count(log_ctx->collect_paths) > 0) + { + log_entry->changed_paths = log_ctx->collect_paths; + log_entry->changed_paths2 = log_ctx->collect_paths; + } + + /* ... and same story for the collected revprops. */ + log_entry->revprops = log_ctx->collect_revprops; + + log_entry->has_children = svn_hash__get_bool(attrs, + "has-children", + FALSE); + log_entry->subtractive_merge = svn_hash__get_bool(attrs, + "subtractive-merge", + FALSE); + + rev_str = svn_hash_gets(attrs, "revision"); + if (rev_str) + log_entry->revision = SVN_STR_TO_REV(rev_str); + else + log_entry->revision = SVN_INVALID_REVNUM; + + /* Give the info to the reporter */ + SVN_ERR(log_ctx->receiver(log_ctx->receiver_baton, + log_entry, + scratch_pool)); + + if (log_entry->has_children) + { + log_ctx->nest_level++; + } + if (! SVN_IS_VALID_REVNUM(log_entry->revision)) + { + SVN_ERR_ASSERT(log_ctx->nest_level); + log_ctx->nest_level--; + } + + /* These hash tables are going to be unusable once this state's + pool is destroyed. But let's not leave stale pointers in + structures that have a longer life. */ + log_ctx->collect_revprops = NULL; + log_ctx->collect_paths = NULL; + } + else if (leaving_state == VERSION) + { + svn_ra_serf__xml_note(xes, ITEM, "revision", cdata->data); + } + else if (leaving_state == CREATOR) + { + if (log_ctx->want_author) + { + SVN_ERR(collect_revprop(log_ctx->collect_revprops, + SVN_PROP_REVISION_AUTHOR, + cdata, + svn_hash_gets(attrs, "encoding"))); + } + } + else if (leaving_state == DATE) + { + if (log_ctx->want_date) + { + SVN_ERR(collect_revprop(log_ctx->collect_revprops, + SVN_PROP_REVISION_DATE, + cdata, + svn_hash_gets(attrs, "encoding"))); + } + } + else if (leaving_state == COMMENT) + { + if (log_ctx->want_message) + { + SVN_ERR(collect_revprop(log_ctx->collect_revprops, + SVN_PROP_REVISION_LOG, + cdata, + svn_hash_gets(attrs, "encoding"))); + } + } + else if (leaving_state == REVPROP) + { + apr_pool_t *result_pool = apr_hash_pool_get(log_ctx->collect_revprops); + + SVN_ERR(collect_revprop( + log_ctx->collect_revprops, + apr_pstrdup(result_pool, + svn_hash_gets(attrs, "name")), + cdata, + svn_hash_gets(attrs, "encoding") + )); + } + else if (leaving_state == HAS_CHILDREN) + { + svn_ra_serf__xml_note(xes, ITEM, "has-children", "yes"); + } + else if (leaving_state == SUBTRACTIVE_MERGE) + { + svn_ra_serf__xml_note(xes, ITEM, "subtractive-merge", "yes"); + } + else + { + char action; + + if (leaving_state == ADDED_PATH) + action = 'A'; + else if (leaving_state == REPLACED_PATH) + action = 'R'; + else if (leaving_state == DELETED_PATH) + action = 'D'; + else + { + SVN_ERR_ASSERT(leaving_state == MODIFIED_PATH); + action = 'M'; + } + + SVN_ERR(collect_path(log_ctx->collect_paths, action, cdata, attrs)); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +create_log_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *buckets; + log_context_t *log_ctx = baton; + + buckets = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(buckets, alloc, + "S:log-report", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + svn_ra_serf__add_tag_buckets(buckets, + "S:start-revision", + apr_ltoa(pool, log_ctx->start), + alloc); + svn_ra_serf__add_tag_buckets(buckets, + "S:end-revision", + apr_ltoa(pool, log_ctx->end), + alloc); + + if (log_ctx->limit) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:limit", apr_ltoa(pool, log_ctx->limit), + alloc); + } + + if (log_ctx->changed_paths) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:discover-changed-paths", NULL, + alloc); + } + + if (log_ctx->strict_node_history) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:strict-node-history", NULL, + alloc); + } + + if (log_ctx->include_merged_revisions) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:include-merged-revisions", NULL, + alloc); + } + + if (log_ctx->revprops) + { + int i; + for (i = 0; i < log_ctx->revprops->nelts; i++) + { + char *name = APR_ARRAY_IDX(log_ctx->revprops, i, char *); + svn_ra_serf__add_tag_buckets(buckets, + "S:revprop", name, + alloc); + } + if (log_ctx->revprops->nelts == 0) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:no-revprops", NULL, + alloc); + } + } + else + { + svn_ra_serf__add_tag_buckets(buckets, + "S:all-revprops", NULL, + alloc); + } + + if (log_ctx->paths) + { + int i; + for (i = 0; i < log_ctx->paths->nelts; i++) + { + svn_ra_serf__add_tag_buckets(buckets, + "S:path", APR_ARRAY_IDX(log_ctx->paths, i, + const char*), + alloc); + } + } + + svn_ra_serf__add_tag_buckets(buckets, + "S:encode-binary-props", NULL, + alloc); + + svn_ra_serf__add_close_tag_buckets(buckets, alloc, + "S:log-report"); + + *body_bkt = buckets; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_log(svn_ra_session_t *ra_session, + 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_log_entry_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool) +{ + log_context_t *log_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + svn_boolean_t want_custom_revprops; + svn_revnum_t peg_rev; + svn_error_t *err; + const char *req_url; + + log_ctx = apr_pcalloc(pool, sizeof(*log_ctx)); + log_ctx->pool = pool; + log_ctx->receiver = receiver; + log_ctx->receiver_baton = receiver_baton; + log_ctx->paths = paths; + log_ctx->start = start; + log_ctx->end = end; + log_ctx->limit = limit; + log_ctx->changed_paths = discover_changed_paths; + log_ctx->strict_node_history = strict_node_history; + log_ctx->include_merged_revisions = include_merged_revisions; + log_ctx->revprops = revprops; + log_ctx->nest_level = 0; + + want_custom_revprops = FALSE; + if (revprops) + { + int i; + for (i = 0; i < revprops->nelts; i++) + { + char *name = APR_ARRAY_IDX(revprops, i, char *); + if (strcmp(name, SVN_PROP_REVISION_AUTHOR) == 0) + log_ctx->want_author = TRUE; + else if (strcmp(name, SVN_PROP_REVISION_DATE) == 0) + log_ctx->want_date = TRUE; + else if (strcmp(name, SVN_PROP_REVISION_LOG) == 0) + log_ctx->want_message = TRUE; + else + want_custom_revprops = TRUE; + } + } + else + { + log_ctx->want_author = log_ctx->want_date = log_ctx->want_message = TRUE; + want_custom_revprops = TRUE; + } + + if (want_custom_revprops) + { + svn_boolean_t has_log_revprops; + SVN_ERR(svn_ra_serf__has_capability(ra_session, &has_log_revprops, + SVN_RA_CAPABILITY_LOG_REVPROPS, pool)); + if (!has_log_revprops) + return svn_error_create(SVN_ERR_RA_NOT_IMPLEMENTED, NULL, + _("Server does not support custom revprops" + " via log")); + } + /* At this point, we may have a deleted file. So, we'll match ra_neon's + * behavior and use the larger of start or end as our 'peg' rev. + */ + peg_rev = (start > end) ? start : end; + + SVN_ERR(svn_ra_serf__get_stable_url(&req_url, NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, peg_rev, + pool, pool)); + + xmlctx = svn_ra_serf__xml_context_create(log_ttable, + log_opened, log_closed, NULL, + log_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = req_url; + handler->body_delegate = create_log_body; + handler->body_delegate_baton = log_ctx; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + err = svn_ra_serf__context_run_one(handler, pool); + + SVN_ERR(svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + req_url, + handler->location), + err)); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/merge.c b/subversion/libsvn_ra_serf/merge.c new file mode 100644 index 0000000..670e421 --- /dev/null +++ b/subversion/libsvn_ra_serf/merge.c @@ -0,0 +1,430 @@ +/* + * merge.c : MERGE response parsing functions for ra_serf + * + * ==================================================================== + * 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_uri.h> + +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "svn_config.h" +#include "svn_dirent_uri.h" +#include "svn_props.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_fspath.h" +#include "svn_private_config.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* + * This enum represents the current state of our XML parsing for a MERGE. + */ +typedef enum merge_state_e { + INITIAL = 0, + MERGE_RESPONSE, + UPDATED_SET, + RESPONSE, + HREF, + PROPSTAT, + PROP, + RESOURCE_TYPE, + BASELINE, + COLLECTION, + SKIP_HREF, + CHECKED_IN, + VERSION_NAME, + DATE, + AUTHOR, + POST_COMMIT_ERR, + + PROP_VAL +} merge_state_e; + + +/* Structure associated with a MERGE request. */ +typedef struct merge_context_t +{ + apr_pool_t *pool; + + svn_ra_serf__session_t *session; + svn_ra_serf__handler_t *handler; + + apr_hash_t *lock_tokens; + svn_boolean_t keep_locks; + + const char *merge_resource_url; /* URL of resource to be merged. */ + const char *merge_url; /* URL at which the MERGE request is aimed. */ + + svn_commit_info_t *commit_info; + +} merge_context_t; + + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t merge_ttable[] = { + { INITIAL, D_, "merge-response", MERGE_RESPONSE, + FALSE, { NULL }, FALSE }, + + { MERGE_RESPONSE, D_, "updated-set", UPDATED_SET, + FALSE, { NULL }, FALSE }, + + { UPDATED_SET, D_, "response", RESPONSE, + FALSE, { NULL }, TRUE }, + + { RESPONSE, D_, "href", HREF, + TRUE, { NULL }, TRUE }, + + { RESPONSE, D_, "propstat", PROPSTAT, + FALSE, { NULL }, FALSE }, + +#if 0 + /* Not needed. */ + { PROPSTAT, D_, "status", STATUS, + FALSE, { NULL }, FALSE }, +#endif + + { PROPSTAT, D_, "prop", PROP, + FALSE, { NULL }, FALSE }, + + { PROP, D_, "resourcetype", RESOURCE_TYPE, + FALSE, { NULL }, FALSE }, + + { RESOURCE_TYPE, D_, "baseline", BASELINE, + FALSE, { NULL }, TRUE }, + + { RESOURCE_TYPE, D_, "collection", COLLECTION, + FALSE, { NULL }, TRUE }, + + { PROP, D_, "checked-in", SKIP_HREF, + FALSE, { NULL }, FALSE }, + + { SKIP_HREF, D_, "href", CHECKED_IN, + TRUE, { NULL }, TRUE }, + + { PROP, D_, SVN_DAV__VERSION_NAME, VERSION_NAME, + TRUE, { NULL }, TRUE }, + + { PROP, D_, SVN_DAV__CREATIONDATE, DATE, + TRUE, { NULL }, TRUE }, + + { PROP, D_, "creator-displayname", AUTHOR, + TRUE, { NULL }, TRUE }, + + { PROP, S_, "post-commit-err", POST_COMMIT_ERR, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +merge_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + merge_context_t *merge_ctx = baton; + + if (leaving_state == RESPONSE) + { + const char *rtype; + + rtype = svn_hash_gets(attrs, "resourcetype"); + + /* rtype can only be "baseline" or "collection" (or NULL). We can + keep this check simple. */ + if (rtype && *rtype == 'b') + { + const char *rev_str; + + rev_str = svn_hash_gets(attrs, "revision"); + if (rev_str) + merge_ctx->commit_info->revision = SVN_STR_TO_REV(rev_str); + else + merge_ctx->commit_info->revision = SVN_INVALID_REVNUM; + + merge_ctx->commit_info->date = + apr_pstrdup(merge_ctx->pool, + svn_hash_gets(attrs, "date")); + + merge_ctx->commit_info->author = + apr_pstrdup(merge_ctx->pool, + svn_hash_gets(attrs, "author")); + + merge_ctx->commit_info->post_commit_err = + apr_pstrdup(merge_ctx->pool, + svn_hash_gets(attrs, "post-commit-err")); + } + else + { + const char *href; + + href = svn_urlpath__skip_ancestor( + merge_ctx->merge_url, + svn_hash_gets(attrs, "href")); + + if (href == NULL) + return svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("A MERGE response for '%s' is not " + "a child of the destination ('%s')"), + href, merge_ctx->merge_url); + + /* We now need to dive all the way into the WC to update the + base VCC url. */ + if (!SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(merge_ctx->session) + && merge_ctx->session->wc_callbacks->push_wc_prop) + { + const char *checked_in; + svn_string_t checked_in_str; + + checked_in = svn_hash_gets(attrs, "checked-in"); + checked_in_str.data = checked_in; + checked_in_str.len = strlen(checked_in); + + SVN_ERR(merge_ctx->session->wc_callbacks->push_wc_prop( + merge_ctx->session->wc_callback_baton, + href, + SVN_RA_SERF__WC_CHECKED_IN_URL, + &checked_in_str, + scratch_pool)); + } + } + } + else if (leaving_state == BASELINE) + { + svn_ra_serf__xml_note(xes, RESPONSE, "resourcetype", "baseline"); + } + else if (leaving_state == COLLECTION) + { + svn_ra_serf__xml_note(xes, RESPONSE, "resourcetype", "collection"); + } + else + { + const char *name; + const char *value = cdata->data; + + if (leaving_state == HREF) + { + name = "href"; + value = svn_urlpath__canonicalize(value, scratch_pool); + } + else if (leaving_state == CHECKED_IN) + { + name = "checked-in"; + value = svn_urlpath__canonicalize(value, scratch_pool); + } + else if (leaving_state == VERSION_NAME) + name = "revision"; + else if (leaving_state == DATE) + name = "date"; + else if (leaving_state == AUTHOR) + name = "author"; + else if (leaving_state == POST_COMMIT_ERR) + name = "post-commit-err"; + else + SVN_ERR_MALFUNCTION(); + + svn_ra_serf__xml_note(xes, RESPONSE, name, value); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +setup_merge_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + merge_context_t *ctx = baton; + + if (!ctx->keep_locks) + { + serf_bucket_headers_set(headers, SVN_DAV_OPTIONS_HEADER, + SVN_DAV_OPTION_RELEASE_LOCKS); + } + + return SVN_NO_ERROR; +} + +void +svn_ra_serf__merge_lock_token_list(apr_hash_t *lock_tokens, + const char *parent, + serf_bucket_t *body, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + apr_hash_index_t *hi; + + if (!lock_tokens || apr_hash_count(lock_tokens) == 0) + return; + + svn_ra_serf__add_open_tag_buckets(body, alloc, + "S:lock-token-list", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + for (hi = apr_hash_first(pool, lock_tokens); + hi; + hi = apr_hash_next(hi)) + { + const void *key; + apr_ssize_t klen; + void *val; + svn_string_t path; + + apr_hash_this(hi, &key, &klen, &val); + + path.data = key; + path.len = klen; + + if (parent && !svn_relpath_skip_ancestor(parent, key)) + continue; + + svn_ra_serf__add_open_tag_buckets(body, alloc, "S:lock", NULL); + + svn_ra_serf__add_open_tag_buckets(body, alloc, "lock-path", NULL); + svn_ra_serf__add_cdata_len_buckets(body, alloc, path.data, path.len); + svn_ra_serf__add_close_tag_buckets(body, alloc, "lock-path"); + + svn_ra_serf__add_tag_buckets(body, "lock-token", val, alloc); + + svn_ra_serf__add_close_tag_buckets(body, alloc, "S:lock"); + } + + svn_ra_serf__add_close_tag_buckets(body, alloc, "S:lock-token-list"); +} + +static svn_error_t* +create_merge_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + merge_context_t *ctx = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_xml_header_buckets(body_bkt, alloc); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:merge", + "xmlns:D", "DAV:", + NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:source", NULL); + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:href", NULL); + + svn_ra_serf__add_cdata_len_buckets(body_bkt, alloc, + ctx->merge_resource_url, + strlen(ctx->merge_resource_url)); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:href"); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:source"); + + svn_ra_serf__add_tag_buckets(body_bkt, "D:no-auto-merge", NULL, alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "D:no-checkout", NULL, alloc); + + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, "D:prop", NULL); + svn_ra_serf__add_tag_buckets(body_bkt, "D:checked-in", NULL, alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "D:" SVN_DAV__VERSION_NAME, NULL, alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "D:resourcetype", NULL, alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "D:" SVN_DAV__CREATIONDATE, NULL, alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "D:creator-displayname", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:prop"); + + svn_ra_serf__merge_lock_token_list(ctx->lock_tokens, NULL, body_bkt, alloc, + pool); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "D:merge"); + + *bkt = body_bkt; + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__run_merge(const svn_commit_info_t **commit_info, + int *response_code, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + const char *merge_resource_url, + apr_hash_t *lock_tokens, + svn_boolean_t keep_locks, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + merge_context_t *merge_ctx; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + + merge_ctx = apr_pcalloc(scratch_pool, sizeof(*merge_ctx)); + + merge_ctx->pool = result_pool; + merge_ctx->session = session; + + merge_ctx->merge_resource_url = merge_resource_url; + + merge_ctx->lock_tokens = lock_tokens; + merge_ctx->keep_locks = keep_locks; + + merge_ctx->commit_info = svn_create_commit_info(result_pool); + + merge_ctx->merge_url = session->session_url.path; + + xmlctx = svn_ra_serf__xml_context_create(merge_ttable, + NULL, merge_closed, NULL, + merge_ctx, + scratch_pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, scratch_pool); + + handler->method = "MERGE"; + handler->path = merge_ctx->merge_url; + handler->body_delegate = create_merge_body; + handler->body_delegate_baton = merge_ctx; + handler->conn = conn; + handler->session = session; + + handler->header_delegate = setup_merge_headers; + handler->header_delegate_baton = merge_ctx; + + merge_ctx->handler = handler; + + SVN_ERR(svn_ra_serf__context_run_one(handler, scratch_pool)); + + *commit_info = merge_ctx->commit_info; + *response_code = handler->sline.code; + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/mergeinfo.c b/subversion/libsvn_ra_serf/mergeinfo.c new file mode 100644 index 0000000..b0bf833 --- /dev/null +++ b/subversion/libsvn_ra_serf/mergeinfo.c @@ -0,0 +1,246 @@ +/* + * mergeinfo.c : entry point for mergeinfo RA functions for ra_serf + * + * ==================================================================== + * 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_tables.h> +#include <apr_xml.h> + +#include "svn_hash.h" +#include "svn_mergeinfo.h" +#include "svn_path.h" +#include "svn_ra.h" +#include "svn_string.h" +#include "svn_xml.h" + +#include "private/svn_dav_protocol.h" +#include "../libsvn_ra/ra_loader.h" +#include "svn_private_config.h" +#include "ra_serf.h" + + + + +/* The current state of our XML parsing. */ +typedef enum mergeinfo_state_e { + INITIAL = 0, + MERGEINFO_REPORT, + MERGEINFO_ITEM, + MERGEINFO_PATH, + MERGEINFO_INFO +} mergeinfo_state_e; + +/* Baton for accumulating mergeinfo. RESULT_CATALOG stores the final + mergeinfo catalog result we are going to hand back to the caller of + get_mergeinfo. */ +typedef struct mergeinfo_context_t { + apr_pool_t *pool; + svn_mergeinfo_t result_catalog; + const apr_array_header_t *paths; + svn_revnum_t revision; + svn_mergeinfo_inheritance_t inherit; + svn_boolean_t include_descendants; +} mergeinfo_context_t; + + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t mergeinfo_ttable[] = { + { INITIAL, S_, SVN_DAV__MERGEINFO_REPORT, MERGEINFO_REPORT, + FALSE, { NULL }, FALSE }, + + { MERGEINFO_REPORT, S_, SVN_DAV__MERGEINFO_ITEM, MERGEINFO_ITEM, + FALSE, { NULL }, TRUE }, + + { MERGEINFO_ITEM, S_, SVN_DAV__MERGEINFO_PATH, MERGEINFO_PATH, + TRUE, { NULL }, TRUE }, + + { MERGEINFO_ITEM, S_, SVN_DAV__MERGEINFO_INFO, MERGEINFO_INFO, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +mergeinfo_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + mergeinfo_context_t *mergeinfo_ctx = baton; + + if (leaving_state == MERGEINFO_ITEM) + { + /* Placed here from the child elements. */ + const char *path = svn_hash_gets(attrs, "path"); + const char *info = svn_hash_gets(attrs, "info"); + + if (path != NULL && info != NULL) + { + svn_mergeinfo_t path_mergeinfo; + + /* Correct for naughty servers that send "relative" paths + with leading slashes! */ + if (path[0] == '/') + ++path; + + SVN_ERR(svn_mergeinfo_parse(&path_mergeinfo, info, + mergeinfo_ctx->pool)); + + svn_hash_sets(mergeinfo_ctx->result_catalog, + apr_pstrdup(mergeinfo_ctx->pool, path), + path_mergeinfo); + } + } + else + { + SVN_ERR_ASSERT(leaving_state == MERGEINFO_PATH + || leaving_state == MERGEINFO_INFO); + + /* Stash the value onto the parent MERGEINFO_ITEM. */ + svn_ra_serf__xml_note(xes, MERGEINFO_ITEM, + leaving_state == MERGEINFO_PATH + ? "path" + : "info", + cdata->data); + } + + return SVN_NO_ERROR; +} + + +static svn_error_t * +create_mergeinfo_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + mergeinfo_context_t *mergeinfo_ctx = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "S:" SVN_DAV__MERGEINFO_REPORT, + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + svn_ra_serf__add_tag_buckets(body_bkt, + "S:" SVN_DAV__REVISION, + apr_ltoa(pool, mergeinfo_ctx->revision), + alloc); + svn_ra_serf__add_tag_buckets(body_bkt, "S:" SVN_DAV__INHERIT, + svn_inheritance_to_word(mergeinfo_ctx->inherit), + alloc); + if (mergeinfo_ctx->include_descendants) + { + svn_ra_serf__add_tag_buckets(body_bkt, "S:" + SVN_DAV__INCLUDE_DESCENDANTS, + "yes", alloc); + } + + if (mergeinfo_ctx->paths) + { + int i; + + for (i = 0; i < mergeinfo_ctx->paths->nelts; i++) + { + const char *this_path = APR_ARRAY_IDX(mergeinfo_ctx->paths, + i, const char *); + + svn_ra_serf__add_tag_buckets(body_bkt, "S:" SVN_DAV__PATH, + this_path, alloc); + } + } + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, + "S:" SVN_DAV__MERGEINFO_REPORT); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_mergeinfo(svn_ra_session_t *ra_session, + svn_mergeinfo_catalog_t *catalog, + const apr_array_header_t *paths, + svn_revnum_t revision, + svn_mergeinfo_inheritance_t inherit, + svn_boolean_t include_descendants, + apr_pool_t *pool) +{ + svn_error_t *err, *err2; + mergeinfo_context_t *mergeinfo_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + const char *path; + + *catalog = NULL; + + SVN_ERR(svn_ra_serf__get_stable_url(&path, NULL /* latest_revnum */, + session, NULL /* conn */, + NULL /* url */, revision, + pool, pool)); + + mergeinfo_ctx = apr_pcalloc(pool, sizeof(*mergeinfo_ctx)); + mergeinfo_ctx->pool = pool; + mergeinfo_ctx->result_catalog = apr_hash_make(pool); + mergeinfo_ctx->paths = paths; + mergeinfo_ctx->revision = revision; + mergeinfo_ctx->inherit = inherit; + mergeinfo_ctx->include_descendants = include_descendants; + + xmlctx = svn_ra_serf__xml_context_create(mergeinfo_ttable, + NULL, mergeinfo_closed, NULL, + mergeinfo_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "REPORT"; + handler->path = path; + handler->conn = session->conns[0]; + handler->session = session; + handler->body_delegate = create_mergeinfo_body; + handler->body_delegate_baton = mergeinfo_ctx; + handler->body_type = "text/xml"; + + err = svn_ra_serf__context_run_one(handler, pool); + + err2 = svn_ra_serf__error_on_status(handler->sline.code, handler->path, + handler->location); + if (err2) + { + svn_error_clear(err); + return err2; + } + + SVN_ERR(err); + + if (handler->done && apr_hash_count(mergeinfo_ctx->result_catalog)) + *catalog = mergeinfo_ctx->result_catalog; + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/options.c b/subversion/libsvn_ra_serf/options.c new file mode 100644 index 0000000..0af0b15 --- /dev/null +++ b/subversion/libsvn_ra_serf/options.c @@ -0,0 +1,625 @@ +/* + * options.c : entry point for OPTIONS RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> + +#include <serf.h> + +#include "svn_dirent_uri.h" +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" + +#include "../libsvn_ra/ra_loader.h" +#include "svn_private_config.h" +#include "private/svn_fspath.h" + +#include "ra_serf.h" + + +/* In a debug build, setting this environment variable to "yes" will force + the client to speak v1, even if the server is capable of speaking v2. */ +#define SVN_IGNORE_V2_ENV_VAR "SVN_I_LIKE_LATENCY_SO_IGNORE_HTTPV2" + + +/* + * This enum represents the current state of our XML parsing for an OPTIONS. + */ +enum options_state_e { + INITIAL = 0, + OPTIONS, + ACTIVITY_COLLECTION, + HREF +}; + +typedef struct options_context_t { + /* pool to allocate memory from */ + apr_pool_t *pool; + + /* Have we extracted options values from the headers already? */ + svn_boolean_t headers_processed; + + svn_ra_serf__session_t *session; + svn_ra_serf__connection_t *conn; + svn_ra_serf__handler_t *handler; + + svn_ra_serf__response_handler_t inner_handler; + void *inner_baton; + + const char *activity_collection; + svn_revnum_t youngest_rev; + +} options_context_t; + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t options_ttable[] = { + { INITIAL, D_, "options-response", OPTIONS, + FALSE, { NULL }, FALSE }, + + { OPTIONS, D_, "activity-collection-set", ACTIVITY_COLLECTION, + FALSE, { NULL }, FALSE }, + + { ACTIVITY_COLLECTION, D_, "href", HREF, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +options_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + options_context_t *opt_ctx = baton; + + SVN_ERR_ASSERT(leaving_state == HREF); + SVN_ERR_ASSERT(cdata != NULL); + + opt_ctx->activity_collection = svn_urlpath__canonicalize(cdata->data, + opt_ctx->pool); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +create_options_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + serf_bucket_t *body; + body = serf_bucket_aggregate_create(alloc); + svn_ra_serf__add_xml_header_buckets(body, alloc); + svn_ra_serf__add_open_tag_buckets(body, alloc, "D:options", + "xmlns:D", "DAV:", + NULL); + svn_ra_serf__add_tag_buckets(body, "D:activity-collection-set", NULL, alloc); + svn_ra_serf__add_close_tag_buckets(body, alloc, "D:options"); + + *body_bkt = body; + return SVN_NO_ERROR; +} + + +/* We use these static pointers so we can employ pointer comparison + * of our capabilities hash members instead of strcmp()ing all over + * the place. + */ +/* Both server and repository support the capability. */ +static const char *const capability_yes = "yes"; +/* Either server or repository does not support the capability. */ +static const char *const capability_no = "no"; +/* Server supports the capability, but don't yet know if repository does. */ +static const char *const capability_server_yes = "server-yes"; + + +/* This implements serf_bucket_headers_do_callback_fn_t. + */ +static int +capabilities_headers_iterator_callback(void *baton, + const char *key, + const char *val) +{ + options_context_t *opt_ctx = baton; + svn_ra_serf__session_t *session = opt_ctx->session; + + if (svn_cstring_casecmp(key, "dav") == 0) + { + /* Each header may contain multiple values, separated by commas, e.g.: + DAV: version-control,checkout,working-resource + DAV: merge,baseline,activity,version-controlled-collection + DAV: http://subversion.tigris.org/xmlns/dav/svn/depth */ + apr_array_header_t *vals = svn_cstring_split(val, ",", TRUE, + opt_ctx->pool); + + /* Right now we only have a few capabilities to detect, so just + seek for them directly. This could be written slightly more + efficiently, but that wouldn't be worth it until we have many + more capabilities. */ + + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_DEPTH, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_DEPTH, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_MERGEINFO, vals)) + { + /* The server doesn't know what repository we're referring + to, so it can't just say capability_yes. */ + if (!svn_hash_gets(session->capabilities, + SVN_RA_CAPABILITY_MERGEINFO)) + { + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO, + capability_server_yes); + } + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_LOG_REVPROPS, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_LOG_REVPROPS, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_ATOMIC_REVPROPS, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_ATOMIC_REVPROPS, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_PARTIAL_REPLAY, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_PARTIAL_REPLAY, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_INHERITED_PROPS, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_INHERITED_PROPS, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_REVERSE_FILE_REVS, + vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_GET_FILE_REVS_REVERSE, + capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_EPHEMERAL_TXNPROPS, vals)) + { + svn_hash_sets(session->capabilities, + SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS, capability_yes); + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_INLINE_PROPS, vals)) + { + session->supports_inline_props = TRUE; + } + if (svn_cstring_match_list(SVN_DAV_NS_DAV_SVN_REPLAY_REV_RESOURCE, vals)) + { + session->supports_rev_rsrc_replay = TRUE; + } + } + + /* SVN-specific headers -- if present, server supports HTTP protocol v2 */ + else if (strncmp(key, "SVN", 3) == 0) + { + /* If we've not yet seen any information about supported POST + requests, we'll initialize the list/hash with "create-txn" + (which we know is supported by virtue of the server speaking + HTTPv2 at all. */ + if (! session->supported_posts) + { + session->supported_posts = apr_hash_make(session->pool); + apr_hash_set(session->supported_posts, "create-txn", 10, (void *)1); + } + + if (svn_cstring_casecmp(key, SVN_DAV_ROOT_URI_HEADER) == 0) + { + session->repos_root = session->session_url; + session->repos_root.path = + (char *)svn_fspath__canonicalize(val, session->pool); + session->repos_root_str = + svn_urlpath__canonicalize( + apr_uri_unparse(session->pool, &session->repos_root, 0), + session->pool); + } + else if (svn_cstring_casecmp(key, SVN_DAV_ME_RESOURCE_HEADER) == 0) + { +#ifdef SVN_DEBUG + char *ignore_v2_env_var = getenv(SVN_IGNORE_V2_ENV_VAR); + + if (!(ignore_v2_env_var + && apr_strnatcasecmp(ignore_v2_env_var, "yes") == 0)) + session->me_resource = apr_pstrdup(session->pool, val); +#else + session->me_resource = apr_pstrdup(session->pool, val); +#endif + } + else if (svn_cstring_casecmp(key, SVN_DAV_REV_STUB_HEADER) == 0) + { + session->rev_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_REV_ROOT_STUB_HEADER) == 0) + { + session->rev_root_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_TXN_STUB_HEADER) == 0) + { + session->txn_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_TXN_ROOT_STUB_HEADER) == 0) + { + session->txn_root_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_VTXN_STUB_HEADER) == 0) + { + session->vtxn_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_VTXN_ROOT_STUB_HEADER) == 0) + { + session->vtxn_root_stub = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_REPOS_UUID_HEADER) == 0) + { + session->uuid = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_YOUNGEST_REV_HEADER) == 0) + { + opt_ctx->youngest_rev = SVN_STR_TO_REV(val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_ALLOW_BULK_UPDATES) == 0) + { + session->server_allows_bulk = apr_pstrdup(session->pool, val); + } + else if (svn_cstring_casecmp(key, SVN_DAV_SUPPORTED_POSTS_HEADER) == 0) + { + /* May contain multiple values, separated by commas. */ + int i; + apr_array_header_t *vals = svn_cstring_split(val, ",", TRUE, + opt_ctx->pool); + + for (i = 0; i < vals->nelts; i++) + { + const char *post_val = APR_ARRAY_IDX(vals, i, const char *); + + svn_hash_sets(session->supported_posts, post_val, (void *)1); + } + } + else if (svn_cstring_casecmp(key, SVN_DAV_REPOSITORY_MERGEINFO) == 0) + { + if (svn_cstring_casecmp(val, "yes") == 0) + { + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO, + capability_yes); + } + else if (svn_cstring_casecmp(val, "no") == 0) + { + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO, + capability_no); + } + } + } + + return 0; +} + + +/* A custom serf_response_handler_t which is mostly a wrapper around + the expat-based response handler -- it just notices OPTIONS response + headers first, before handing off to the xml parser. + Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +options_response_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool) +{ + options_context_t *opt_ctx = baton; + + if (!opt_ctx->headers_processed) + { + svn_ra_serf__session_t *session = opt_ctx->session; + serf_bucket_t *hdrs = serf_bucket_response_get_headers(response); + + /* Start out assuming all capabilities are unsupported. */ + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_PARTIAL_REPLAY, + capability_no); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_DEPTH, + capability_no); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO, + NULL); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_LOG_REVPROPS, + capability_no); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_ATOMIC_REVPROPS, + capability_no); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_INHERITED_PROPS, + capability_no); + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_EPHEMERAL_TXNPROPS, + capability_no); + + /* Then see which ones we can discover. */ + serf_bucket_headers_do(hdrs, capabilities_headers_iterator_callback, + opt_ctx); + + /* Assume mergeinfo capability unsupported, if didn't recieve information + about server or repository mergeinfo capability. */ + if (!svn_hash_gets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO)) + svn_hash_sets(session->capabilities, SVN_RA_CAPABILITY_MERGEINFO, + capability_no); + + opt_ctx->headers_processed = TRUE; + } + + /* Execute the 'real' response handler to XML-parse the response body. */ + return opt_ctx->inner_handler(request, response, opt_ctx->inner_baton, pool); +} + + +static svn_error_t * +create_options_req(options_context_t **opt_ctx, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool) +{ + options_context_t *new_ctx; + svn_ra_serf__xml_context_t *xmlctx; + svn_ra_serf__handler_t *handler; + + new_ctx = apr_pcalloc(pool, sizeof(*new_ctx)); + new_ctx->pool = pool; + new_ctx->session = session; + new_ctx->conn = conn; + + new_ctx->youngest_rev = SVN_INVALID_REVNUM; + + xmlctx = svn_ra_serf__xml_context_create(options_ttable, + NULL, options_closed, NULL, + new_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "OPTIONS"; + handler->path = session->session_url.path; + handler->body_delegate = create_options_body; + handler->body_type = "text/xml"; + handler->conn = conn; + handler->session = session; + + new_ctx->handler = handler; + + new_ctx->inner_handler = handler->response_handler; + new_ctx->inner_baton = handler->response_baton; + handler->response_handler = options_response_handler; + handler->response_baton = new_ctx; + + *opt_ctx = new_ctx; + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__v2_get_youngest_revnum(svn_revnum_t *youngest, + svn_ra_serf__connection_t *conn, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = conn->session; + options_context_t *opt_ctx; + + SVN_ERR_ASSERT(SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)); + + SVN_ERR(create_options_req(&opt_ctx, session, conn, scratch_pool)); + SVN_ERR(svn_ra_serf__context_run_one(opt_ctx->handler, scratch_pool)); + SVN_ERR(svn_ra_serf__error_on_status(opt_ctx->handler->sline.code, + opt_ctx->handler->path, + opt_ctx->handler->location)); + + *youngest = opt_ctx->youngest_rev; + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__v1_get_activity_collection(const char **activity_url, + svn_ra_serf__connection_t *conn, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = conn->session; + options_context_t *opt_ctx; + + SVN_ERR_ASSERT(!SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)); + + SVN_ERR(create_options_req(&opt_ctx, session, conn, scratch_pool)); + SVN_ERR(svn_ra_serf__context_run_one(opt_ctx->handler, scratch_pool)); + + SVN_ERR(svn_ra_serf__error_on_status(opt_ctx->handler->sline.code, + opt_ctx->handler->path, + opt_ctx->handler->location)); + + *activity_url = apr_pstrdup(result_pool, opt_ctx->activity_collection); + + return SVN_NO_ERROR; + +} + + + +/** Capabilities exchange. */ + +svn_error_t * +svn_ra_serf__exchange_capabilities(svn_ra_serf__session_t *serf_sess, + const char **corrected_url, + apr_pool_t *pool) +{ + options_context_t *opt_ctx; + svn_error_t *err; + + /* This routine automatically fills in serf_sess->capabilities */ + SVN_ERR(create_options_req(&opt_ctx, serf_sess, serf_sess->conns[0], pool)); + + err = svn_ra_serf__context_run_one(opt_ctx->handler, pool); + + /* If our caller cares about server redirections, and our response + carries such a thing, report as much. We'll disregard ERR -- + it's most likely just a complaint about the response body not + successfully parsing as XML or somesuch. */ + if (corrected_url && (opt_ctx->handler->sline.code == 301)) + { + svn_error_clear(err); + *corrected_url = opt_ctx->handler->location; + return SVN_NO_ERROR; + } + + SVN_ERR(svn_error_compose_create( + svn_ra_serf__error_on_status(opt_ctx->handler->sline.code, + serf_sess->session_url.path, + opt_ctx->handler->location), + err)); + + /* Opportunistically cache any reported activity URL. (We don't + want to have to ask for this again later, potentially against an + unreadable commit anchor URL.) */ + if (opt_ctx->activity_collection) + { + serf_sess->activity_collection_url = + apr_pstrdup(serf_sess->pool, opt_ctx->activity_collection); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__has_capability(svn_ra_session_t *ra_session, + svn_boolean_t *has, + const char *capability, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *serf_sess = ra_session->priv; + const char *cap_result; + + /* This capability doesn't rely on anything server side. */ + if (strcmp(capability, SVN_RA_CAPABILITY_COMMIT_REVPROPS) == 0) + { + *has = TRUE; + return SVN_NO_ERROR; + } + + cap_result = svn_hash_gets(serf_sess->capabilities, capability); + + /* If any capability is unknown, they're all unknown, so ask. */ + if (cap_result == NULL) + SVN_ERR(svn_ra_serf__exchange_capabilities(serf_sess, NULL, pool)); + + /* Try again, now that we've fetched the capabilities. */ + cap_result = svn_hash_gets(serf_sess->capabilities, capability); + + /* Some capabilities depend on the repository as well as the server. */ + if (cap_result == capability_server_yes) + { + if (strcmp(capability, SVN_RA_CAPABILITY_MERGEINFO) == 0) + { + /* Handle mergeinfo specially. Mergeinfo depends on the + repository as well as the server, but the server routine + that answered our svn_ra_serf__exchange_capabilities() call above + didn't even know which repository we were interested in + -- it just told us whether the server supports mergeinfo. + If the answer was 'no', there's no point checking the + particular repository; but if it was 'yes', we still must + change it to 'no' iff the repository itself doesn't + support mergeinfo. */ + svn_mergeinfo_catalog_t ignored; + svn_error_t *err; + apr_array_header_t *paths = apr_array_make(pool, 1, + sizeof(char *)); + APR_ARRAY_PUSH(paths, const char *) = ""; + + err = svn_ra_serf__get_mergeinfo(ra_session, &ignored, paths, 0, + FALSE, FALSE, pool); + + if (err) + { + if (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE) + { + svn_error_clear(err); + cap_result = capability_no; + } + else if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + /* Mergeinfo requests use relative paths, and + anyway we're in r0, so this is a likely error, + but it means the repository supports mergeinfo! */ + svn_error_clear(err); + cap_result = capability_yes; + } + else + return err; + } + else + cap_result = capability_yes; + + svn_hash_sets(serf_sess->capabilities, + SVN_RA_CAPABILITY_MERGEINFO, cap_result); + } + else + { + return svn_error_createf + (SVN_ERR_UNKNOWN_CAPABILITY, NULL, + _("Don't know how to handle '%s' for capability '%s'"), + capability_server_yes, capability); + } + } + + if (cap_result == capability_yes) + { + *has = TRUE; + } + else if (cap_result == capability_no) + { + *has = FALSE; + } + else if (cap_result == NULL) + { + return svn_error_createf + (SVN_ERR_UNKNOWN_CAPABILITY, NULL, + _("Don't know anything about capability '%s'"), capability); + } + else /* "can't happen" */ + { + /* Well, let's hope it's a string. */ + return svn_error_createf + (SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("Attempt to fetch capability '%s' resulted in '%s'"), + capability, cap_result); + } + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/property.c b/subversion/libsvn_ra_serf/property.c new file mode 100644 index 0000000..63972e8 --- /dev/null +++ b/subversion/libsvn_ra_serf/property.c @@ -0,0 +1,1263 @@ +/* + * property.c : property routines for ra_serf + * + * ==================================================================== + * 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 <serf.h> + +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_base64.h" +#include "svn_xml.h" +#include "svn_props.h" +#include "svn_dirent_uri.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_fspath.h" +#include "private/svn_string_private.h" +#include "svn_private_config.h" + +#include "ra_serf.h" + + +/* Our current parsing state we're in for the PROPFIND response. */ +typedef enum prop_state_e { + INITIAL = 0, + MULTISTATUS, + RESPONSE, + HREF, + PROPSTAT, + STATUS, + PROP, + PROPVAL, + COLLECTION, + HREF_VALUE +} prop_state_e; + + +/* + * This structure represents a pending PROPFIND response. + */ +typedef struct propfind_context_t { + /* pool to issue allocations from */ + apr_pool_t *pool; + + svn_ra_serf__handler_t *handler; + + /* associated serf session */ + svn_ra_serf__session_t *sess; + svn_ra_serf__connection_t *conn; + + /* the requested path */ + const char *path; + + /* the requested version (number and string form) */ + svn_revnum_t rev; + const char *label; + + /* the request depth */ + const char *depth; + + /* the list of requested properties */ + const svn_ra_serf__dav_props_t *find_props; + + /* hash table that will be updated with the properties + * + * This can be shared between multiple propfind_context_t + * structures + */ + apr_hash_t *ret_props; + + /* hash table containing all the properties associated with the + * "current" <propstat> tag. These will get copied into RET_PROPS + * if the status code similarly associated indicates that they are + * "good"; otherwise, they'll get discarded. + */ + apr_hash_t *ps_props; + + /* If not-NULL, add us to this list when we're done. */ + svn_ra_serf__list_t **done_list; + + svn_ra_serf__list_t done_item; + +} propfind_context_t; + + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t propfind_ttable[] = { + { INITIAL, D_, "multistatus", MULTISTATUS, + FALSE, { NULL }, TRUE }, + + { MULTISTATUS, D_, "response", RESPONSE, + FALSE, { NULL }, FALSE }, + + { RESPONSE, D_, "href", HREF, + TRUE, { NULL }, TRUE }, + + { RESPONSE, D_, "propstat", PROPSTAT, + FALSE, { NULL }, TRUE }, + + { PROPSTAT, D_, "status", STATUS, + TRUE, { NULL }, TRUE }, + + { PROPSTAT, D_, "prop", PROP, + FALSE, { NULL }, FALSE }, + + { PROP, "*", "*", PROPVAL, + TRUE, { "?V:encoding", NULL }, TRUE }, + + { PROPVAL, D_, "collection", COLLECTION, + FALSE, { NULL }, TRUE }, + + { PROPVAL, D_, "href", HREF_VALUE, + TRUE, { NULL }, TRUE }, + + { 0 } +}; + + +/* Return the HTTP status code contained in STATUS_LINE, or 0 if + there's a problem parsing it. */ +static int parse_status_code(const char *status_line) +{ + /* STATUS_LINE should be of form: "HTTP/1.1 200 OK" */ + if (status_line[0] == 'H' && + status_line[1] == 'T' && + status_line[2] == 'T' && + status_line[3] == 'P' && + status_line[4] == '/' && + (status_line[5] >= '0' && status_line[5] <= '9') && + status_line[6] == '.' && + (status_line[7] >= '0' && status_line[7] <= '9') && + status_line[8] == ' ') + { + char *reason; + + return apr_strtoi64(status_line + 8, &reason, 10); + } + return 0; +} + + +/* Conforms to svn_ra_serf__path_rev_walker_t */ +static svn_error_t * +copy_into_ret_props(void *baton, + const char *path, apr_ssize_t path_len, + const char *ns, apr_ssize_t ns_len, + const char *name, apr_ssize_t name_len, + const svn_string_t *val, + apr_pool_t *pool) +{ + propfind_context_t *ctx = baton; + + svn_ra_serf__set_ver_prop(ctx->ret_props, path, ctx->rev, ns, name, + val, ctx->pool); + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_opened_t */ +static svn_error_t * +propfind_opened(svn_ra_serf__xml_estate_t *xes, + void *baton, + int entered_state, + const svn_ra_serf__dav_props_t *tag, + apr_pool_t *scratch_pool) +{ + propfind_context_t *ctx = baton; + + if (entered_state == PROPVAL) + { + svn_ra_serf__xml_note(xes, PROPVAL, "ns", tag->namespace); + svn_ra_serf__xml_note(xes, PROPVAL, "name", tag->name); + } + else if (entered_state == PROPSTAT) + { + ctx->ps_props = apr_hash_make(ctx->pool); + } + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +propfind_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + propfind_context_t *ctx = baton; + + if (leaving_state == MULTISTATUS) + { + /* We've gathered all the data from the reponse. Add this item + onto the "done list". External callers will then know this + request has been completed (tho stray response bytes may still + arrive). */ + if (ctx->done_list) + { + ctx->done_item.data = ctx->handler; + ctx->done_item.next = *ctx->done_list; + *ctx->done_list = &ctx->done_item; + } + } + else if (leaving_state == HREF) + { + const char *path; + const svn_string_t *val_str; + + if (strcmp(ctx->depth, "1") == 0) + path = svn_urlpath__canonicalize(cdata->data, scratch_pool); + else + path = ctx->path; + + svn_ra_serf__xml_note(xes, RESPONSE, "path", path); + + /* Copy the value into the right pool, then save the HREF. */ + val_str = svn_string_dup(cdata, ctx->pool); + svn_ra_serf__set_ver_prop(ctx->ret_props, + path, ctx->rev, D_, "href", val_str, + ctx->pool); + } + else if (leaving_state == COLLECTION) + { + svn_ra_serf__xml_note(xes, PROPVAL, "altvalue", "collection"); + } + else if (leaving_state == HREF_VALUE) + { + svn_ra_serf__xml_note(xes, PROPVAL, "altvalue", cdata->data); + } + else if (leaving_state == STATUS) + { + /* Parse the status field, and remember if this is a property + that we wish to ignore. (Typically, if it's not a 200, the + status will be 404 to indicate that a property we + specifically requested from the server doesn't exist.) */ + int status = parse_status_code(cdata->data); + if (status != 200) + svn_ra_serf__xml_note(xes, PROPSTAT, "ignore-prop", "*"); + } + else if (leaving_state == PROPVAL) + { + const char *encoding = svn_hash_gets(attrs, "V:encoding"); + const svn_string_t *val_str; + apr_hash_t *gathered; + const char *path; + const char *ns; + const char *name; + const char *altvalue; + + if (encoding) + { + if (strcmp(encoding, "base64") != 0) + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, + NULL, + _("Got unrecognized encoding '%s'"), + encoding); + + /* Decode into the right pool. */ + val_str = svn_base64_decode_string(cdata, ctx->pool); + } + else + { + /* Copy into the right pool. */ + val_str = svn_string_dup(cdata, ctx->pool); + } + + /* The current path sits on the RESPONSE state. Gather up all the + state from this PROPVAL to the (grandparent) RESPONSE state, + and grab the path from there. + + Now, it would be nice if we could, at this point, know that + the status code for this property indicated a problem -- then + we could simply bail out here and ignore the property. + Sadly, though, we might get the status code *after* we get + the property value. So we'll carry on with our processing + here, setting the property and value as expected. Once we + know for sure the status code associate with the property, + we'll decide its fate. */ + gathered = svn_ra_serf__xml_gather_since(xes, RESPONSE); + + /* These will be dup'd into CTX->POOL, as necessary. */ + path = svn_hash_gets(gathered, "path"); + if (path == NULL) + path = ctx->path; + + ns = svn_hash_gets(attrs, "ns"); + name = apr_pstrdup(ctx->pool, + svn_hash_gets(attrs, "name")); + + altvalue = svn_hash_gets(attrs, "altvalue"); + if (altvalue != NULL) + val_str = svn_string_create(altvalue, ctx->pool); + + svn_ra_serf__set_ver_prop(ctx->ps_props, + path, ctx->rev, ns, name, val_str, + ctx->pool); + } + else + { + apr_hash_t *gathered; + + SVN_ERR_ASSERT(leaving_state == PROPSTAT); + + gathered = svn_ra_serf__xml_gather_since(xes, PROPSTAT); + + /* If we've squirreled away a note that says we want to ignore + these properties, we'll do so. Otherwise, we need to copy + them from the temporary hash into the ctx->ret_props hash. */ + if (! svn_hash_gets(gathered, "ignore-prop")) + { + SVN_ERR(svn_ra_serf__walk_all_paths(ctx->ps_props, ctx->rev, + copy_into_ret_props, ctx, + scratch_pool)); + } + + ctx->ps_props = NULL; + } + + return SVN_NO_ERROR; +} + + +const svn_string_t * +svn_ra_serf__get_ver_prop_string(apr_hash_t *props, + const char *path, + svn_revnum_t rev, + const char *ns, + const char *name) +{ + apr_hash_t *ver_props, *path_props, *ns_props; + void *val = NULL; + + ver_props = apr_hash_get(props, &rev, sizeof(rev)); + if (ver_props) + { + path_props = svn_hash_gets(ver_props, path); + + if (path_props) + { + ns_props = svn_hash_gets(path_props, ns); + if (ns_props) + { + val = svn_hash_gets(ns_props, name); + } + } + } + + return val; +} + +const char * +svn_ra_serf__get_ver_prop(apr_hash_t *props, + const char *path, + svn_revnum_t rev, + const char *ns, + const char *name) +{ + const svn_string_t *val; + + val = svn_ra_serf__get_ver_prop_string(props, path, rev, ns, name); + + if (val) + { + return val->data; + } + + return NULL; +} + +const svn_string_t * +svn_ra_serf__get_prop_string(apr_hash_t *props, + const char *path, + const char *ns, + const char *name) +{ + return svn_ra_serf__get_ver_prop_string(props, path, SVN_INVALID_REVNUM, + ns, name); +} + +const char * +svn_ra_serf__get_prop(apr_hash_t *props, + const char *path, + const char *ns, + const char *name) +{ + return svn_ra_serf__get_ver_prop(props, path, SVN_INVALID_REVNUM, ns, name); +} + +void +svn_ra_serf__set_ver_prop(apr_hash_t *props, + const char *path, svn_revnum_t rev, + const char *ns, const char *name, + const svn_string_t *val, apr_pool_t *pool) +{ + apr_hash_t *ver_props, *path_props, *ns_props; + + ver_props = apr_hash_get(props, &rev, sizeof(rev)); + if (!ver_props) + { + ver_props = apr_hash_make(pool); + apr_hash_set(props, apr_pmemdup(pool, &rev, sizeof(rev)), sizeof(rev), + ver_props); + } + + path_props = svn_hash_gets(ver_props, path); + + if (!path_props) + { + path_props = apr_hash_make(pool); + path = apr_pstrdup(pool, path); + svn_hash_sets(ver_props, path, path_props); + + /* todo: we know that we'll fail the next check, but fall through + * for now for simplicity's sake. + */ + } + + ns_props = svn_hash_gets(path_props, ns); + if (!ns_props) + { + ns_props = apr_hash_make(pool); + ns = apr_pstrdup(pool, ns); + svn_hash_sets(path_props, ns, ns_props); + } + + svn_hash_sets(ns_props, name, val); +} + +void +svn_ra_serf__set_prop(apr_hash_t *props, + const char *path, + const char *ns, const char *name, + const svn_string_t *val, apr_pool_t *pool) +{ + svn_ra_serf__set_ver_prop(props, path, SVN_INVALID_REVNUM, ns, name, + val, pool); +} + + +static svn_error_t * +setup_propfind_headers(serf_bucket_t *headers, + void *setup_baton, + apr_pool_t *pool) +{ + propfind_context_t *ctx = setup_baton; + + serf_bucket_headers_setn(headers, "Depth", ctx->depth); + if (ctx->label) + { + serf_bucket_headers_setn(headers, "Label", ctx->label); + } + + return SVN_NO_ERROR; +} + +#define PROPFIND_HEADER "<?xml version=\"1.0\" encoding=\"utf-8\"?><propfind xmlns=\"DAV:\">" +#define PROPFIND_TRAILER "</propfind>" + +static svn_error_t * +create_propfind_body(serf_bucket_t **bkt, + void *setup_baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + propfind_context_t *ctx = setup_baton; + + serf_bucket_t *body_bkt, *tmp; + const svn_ra_serf__dav_props_t *prop; + svn_boolean_t requested_allprop = FALSE; + + body_bkt = serf_bucket_aggregate_create(alloc); + + prop = ctx->find_props; + while (prop && prop->namespace) + { + /* special case the allprop case. */ + if (strcmp(prop->name, "allprop") == 0) + { + requested_allprop = TRUE; + } + + /* <*propname* xmlns="*propns*" /> */ + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("<", 1, alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(prop->name, alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(" xmlns=\"", + sizeof(" xmlns=\"")-1, + alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(prop->namespace, alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("\"/>", sizeof("\"/>")-1, + alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + prop++; + } + + /* If we're not doing an allprop, add <prop> tags. */ + if (!requested_allprop) + { + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("<prop>", + sizeof("<prop>")-1, + alloc); + serf_bucket_aggregate_prepend(body_bkt, tmp); + } + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(PROPFIND_HEADER, + sizeof(PROPFIND_HEADER)-1, + alloc); + + serf_bucket_aggregate_prepend(body_bkt, tmp); + + if (!requested_allprop) + { + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("</prop>", + sizeof("</prop>")-1, + alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + } + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(PROPFIND_TRAILER, + sizeof(PROPFIND_TRAILER)-1, + alloc); + serf_bucket_aggregate_append(body_bkt, tmp); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__deliver_props(svn_ra_serf__handler_t **propfind_handler, + apr_hash_t *ret_props, + svn_ra_serf__session_t *sess, + svn_ra_serf__connection_t *conn, + const char *path, + svn_revnum_t rev, + const char *depth, + const svn_ra_serf__dav_props_t *find_props, + svn_ra_serf__list_t **done_list, + apr_pool_t *pool) +{ + propfind_context_t *new_prop_ctx; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_context_t *xmlctx; + + new_prop_ctx = apr_pcalloc(pool, sizeof(*new_prop_ctx)); + + new_prop_ctx->pool = apr_hash_pool_get(ret_props); + new_prop_ctx->path = path; + new_prop_ctx->find_props = find_props; + new_prop_ctx->ret_props = ret_props; + new_prop_ctx->depth = depth; + new_prop_ctx->sess = sess; + new_prop_ctx->conn = conn; + new_prop_ctx->rev = rev; + new_prop_ctx->done_list = done_list; + + if (SVN_IS_VALID_REVNUM(rev)) + { + new_prop_ctx->label = apr_ltoa(pool, rev); + } + else + { + new_prop_ctx->label = NULL; + } + + xmlctx = svn_ra_serf__xml_context_create(propfind_ttable, + propfind_opened, + propfind_closed, + NULL, + new_prop_ctx, + pool); + handler = svn_ra_serf__create_expat_handler(xmlctx, pool); + + handler->method = "PROPFIND"; + handler->path = path; + handler->body_delegate = create_propfind_body; + handler->body_type = "text/xml"; + handler->body_delegate_baton = new_prop_ctx; + handler->header_delegate = setup_propfind_headers; + handler->header_delegate_baton = new_prop_ctx; + + handler->session = new_prop_ctx->sess; + handler->conn = new_prop_ctx->conn; + + new_prop_ctx->handler = handler; + + *propfind_handler = handler; + + return SVN_NO_ERROR; +} + + +/* + * This helper function will block until the PROP_CTX indicates that is done + * or another error is returned. + */ +svn_error_t * +svn_ra_serf__wait_for_props(svn_ra_serf__handler_t *handler, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + svn_error_t *err2; + + err = svn_ra_serf__context_run_one(handler, scratch_pool); + + err2 = svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location); + + return svn_error_compose_create(err2, err); +} + +/* + * This is a blocking version of deliver_props. + */ +svn_error_t * +svn_ra_serf__retrieve_props(apr_hash_t **results, + svn_ra_serf__session_t *sess, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t rev, + const char *depth, + const svn_ra_serf__dav_props_t *props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t *handler; + + *results = apr_hash_make(result_pool); + + SVN_ERR(svn_ra_serf__deliver_props(&handler, *results, sess, conn, url, + rev, depth, props, NULL, result_pool)); + SVN_ERR(svn_ra_serf__wait_for_props(handler, scratch_pool)); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__fetch_node_props(apr_hash_t **results, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + const svn_ra_serf__dav_props_t *which_props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *multiprops; + apr_hash_t *ver_props; + + /* Note: a couple extra hash tables and whatnot get into RESULT_POOL. + Not a big deal at this point. Theoretically, we could fetch all + props into SCRATCH_POOL, then copy just the REVISION/URL props + into RESULT_POOL. Too much work for too little gain... */ + SVN_ERR(svn_ra_serf__retrieve_props(&multiprops, conn->session, conn, + url, revision, "0", which_props, + result_pool, scratch_pool)); + + ver_props = apr_hash_get(multiprops, &revision, sizeof(revision)); + if (ver_props != NULL) + { + *results = svn_hash_gets(ver_props, url); + if (*results != NULL) + return SVN_NO_ERROR; + } + + return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL, + _("The PROPFIND response did not include " + "the requested properties")); +} + + +svn_error_t * +svn_ra_serf__walk_node_props(apr_hash_t *props, + svn_ra_serf__walker_visitor_t walker, + void *baton, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool; + apr_hash_index_t *ns_hi; + + iterpool = svn_pool_create(scratch_pool); + for (ns_hi = apr_hash_first(scratch_pool, props); ns_hi; + ns_hi = apr_hash_next(ns_hi)) + { + void *ns_val; + const void *ns_name; + apr_hash_index_t *name_hi; + + /* NOTE: We do not clear ITERPOOL in this loop. Generally, there are + very few namespaces, so this loop will not have many iterations. + Instead, ITERPOOL is used for the inner loop. */ + + apr_hash_this(ns_hi, &ns_name, NULL, &ns_val); + + for (name_hi = apr_hash_first(scratch_pool, ns_val); name_hi; + name_hi = apr_hash_next(name_hi)) + { + void *prop_val; + const void *prop_name; + + /* See note above, regarding clearing of this pool. */ + svn_pool_clear(iterpool); + + apr_hash_this(name_hi, &prop_name, NULL, &prop_val); + + SVN_ERR(walker(baton, ns_name, prop_name, prop_val, iterpool)); + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__walk_all_props(apr_hash_t *props, + const char *name, + svn_revnum_t rev, + svn_ra_serf__walker_visitor_t walker, + void *baton, + apr_pool_t *scratch_pool) +{ + apr_hash_t *ver_props; + apr_hash_t *path_props; + + ver_props = apr_hash_get(props, &rev, sizeof(rev)); + if (!ver_props) + return SVN_NO_ERROR; + + path_props = svn_hash_gets(ver_props, name); + if (!path_props) + return SVN_NO_ERROR; + + return svn_error_trace(svn_ra_serf__walk_node_props(path_props, + walker, baton, + scratch_pool)); +} + + +svn_error_t * +svn_ra_serf__walk_all_paths(apr_hash_t *props, + svn_revnum_t rev, + svn_ra_serf__path_rev_walker_t walker, + void *baton, + apr_pool_t *pool) +{ + apr_hash_index_t *path_hi; + apr_hash_t *ver_props; + + ver_props = apr_hash_get(props, &rev, sizeof(rev)); + + if (!ver_props) + { + return SVN_NO_ERROR; + } + + for (path_hi = apr_hash_first(pool, ver_props); path_hi; + path_hi = apr_hash_next(path_hi)) + { + void *path_props; + const void *path_name; + apr_ssize_t path_len; + apr_hash_index_t *ns_hi; + + apr_hash_this(path_hi, &path_name, &path_len, &path_props); + for (ns_hi = apr_hash_first(pool, path_props); ns_hi; + ns_hi = apr_hash_next(ns_hi)) + { + void *ns_val; + const void *ns_name; + apr_ssize_t ns_len; + apr_hash_index_t *name_hi; + apr_hash_this(ns_hi, &ns_name, &ns_len, &ns_val); + for (name_hi = apr_hash_first(pool, ns_val); name_hi; + name_hi = apr_hash_next(name_hi)) + { + void *prop_val; + const void *prop_name; + apr_ssize_t prop_len; + + apr_hash_this(name_hi, &prop_name, &prop_len, &prop_val); + /* use a subpool? */ + SVN_ERR(walker(baton, path_name, path_len, ns_name, ns_len, + prop_name, prop_len, prop_val, pool)); + } + } + } + + return SVN_NO_ERROR; +} + + +const char * +svn_ra_serf__svnname_from_wirename(const char *ns, + const char *name, + apr_pool_t *result_pool) +{ + if (*ns == '\0' || strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0) + return apr_pstrdup(result_pool, name); + + if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0) + return apr_pstrcat(result_pool, SVN_PROP_PREFIX, name, (char *)NULL); + + if (strcmp(ns, SVN_PROP_PREFIX) == 0) + return apr_pstrcat(result_pool, SVN_PROP_PREFIX, name, (char *)NULL); + + if (strcmp(name, SVN_DAV__VERSION_NAME) == 0) + return SVN_PROP_ENTRY_COMMITTED_REV; + + if (strcmp(name, SVN_DAV__CREATIONDATE) == 0) + return SVN_PROP_ENTRY_COMMITTED_DATE; + + if (strcmp(name, "creator-displayname") == 0) + return SVN_PROP_ENTRY_LAST_AUTHOR; + + if (strcmp(name, "repository-uuid") == 0) + return SVN_PROP_ENTRY_UUID; + + if (strcmp(name, "lock-token") == 0) + return SVN_PROP_ENTRY_LOCK_TOKEN; + + if (strcmp(name, "checked-in") == 0) + return SVN_RA_SERF__WC_CHECKED_IN_URL; + + if (strcmp(ns, "DAV:") == 0 || strcmp(ns, SVN_DAV_PROP_NS_DAV) == 0) + { + /* Here DAV: properties not yet converted to svn: properties should be + ignored. */ + return NULL; + } + + /* An unknown namespace, must be a custom property. */ + return apr_pstrcat(result_pool, ns, name, (char *)NULL); +} + + +/* Conforms to svn_ra_serf__walker_visitor_t */ +static svn_error_t * +set_flat_props(void *baton, + const char *ns, + const char *name, + const svn_string_t *value, + apr_pool_t *pool) +{ + apr_hash_t *props = baton; + apr_pool_t *result_pool = apr_hash_pool_get(props); + const char *prop_name; + + /* ### is VAL in the proper pool? */ + + prop_name = svn_ra_serf__svnname_from_wirename(ns, name, result_pool); + if (prop_name != NULL) + svn_hash_sets(props, prop_name, value); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__flatten_props(apr_hash_t **flat_props, + apr_hash_t *props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + *flat_props = apr_hash_make(result_pool); + + return svn_error_trace(svn_ra_serf__walk_node_props( + props, + set_flat_props, + *flat_props /* baton */, + scratch_pool)); +} + + +static svn_error_t * +select_revprops(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + apr_hash_t *revprops = baton; + apr_pool_t *result_pool = apr_hash_pool_get(revprops); + const char *prop_name; + + /* ### copy NAME into the RESULT_POOL? */ + /* ### copy VAL into the RESULT_POOL? */ + + if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0) + prop_name = name; + else if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0) + prop_name = apr_pstrcat(result_pool, SVN_PROP_PREFIX, name, (char *)NULL); + else if (strcmp(ns, SVN_PROP_PREFIX) == 0) + prop_name = apr_pstrcat(result_pool, SVN_PROP_PREFIX, name, (char *)NULL); + else if (strcmp(ns, "") == 0) + prop_name = name; + else + { + /* do nothing for now? */ + return SVN_NO_ERROR; + } + + svn_hash_sets(revprops, prop_name, val); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__select_revprops(apr_hash_t **revprops, + const char *name, + svn_revnum_t rev, + apr_hash_t *all_revprops, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + *revprops = apr_hash_make(result_pool); + + return svn_error_trace(svn_ra_serf__walk_all_props( + all_revprops, name, rev, + select_revprops, *revprops, + scratch_pool)); +} + + +/* + * Contact the server (using CONN) to calculate baseline + * information for BASELINE_URL at REVISION (which may be + * SVN_INVALID_REVNUM to query the HEAD revision). + * + * If ACTUAL_REVISION is non-NULL, set *ACTUAL_REVISION to revision + * retrieved from the server as part of this process (which should + * match REVISION when REVISION is valid). Set *BASECOLL_URL_P to the + * baseline collection URL. + */ +static svn_error_t * +retrieve_baseline_info(svn_revnum_t *actual_revision, + const char **basecoll_url_p, + svn_ra_serf__connection_t *conn, + const char *baseline_url, + svn_revnum_t revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *props; + apr_hash_t *dav_props; + const char *basecoll_url; + + SVN_ERR(svn_ra_serf__fetch_node_props(&props, conn, + baseline_url, revision, + baseline_props, + scratch_pool, scratch_pool)); + dav_props = apr_hash_get(props, "DAV:", 4); + /* If DAV_PROPS is NULL, then svn_prop_get_value() will return NULL. */ + + basecoll_url = svn_prop_get_value(dav_props, "baseline-collection"); + if (!basecoll_url) + { + return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL, + _("The PROPFIND response did not include " + "the requested baseline-collection value")); + } + *basecoll_url_p = svn_urlpath__canonicalize(basecoll_url, result_pool); + + if (actual_revision) + { + const char *version_name; + + version_name = svn_prop_get_value(dav_props, SVN_DAV__VERSION_NAME); + if (!version_name) + return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL, + _("The PROPFIND response did not include " + "the requested version-name value")); + + *actual_revision = SVN_STR_TO_REV(version_name); + } + + return SVN_NO_ERROR; +} + + +/* For HTTPv1 servers, do a PROPFIND dance on the VCC to fetch the youngest + revnum. If BASECOLL_URL is non-NULL, then the corresponding baseline + collection URL is also returned. + + Do the work over CONN. + + *BASECOLL_URL (if requested) will be allocated in RESULT_POOL. All + temporary allocations will be made in SCRATCH_POOL. */ +static svn_error_t * +v1_get_youngest_revnum(svn_revnum_t *youngest, + const char **basecoll_url, + svn_ra_serf__connection_t *conn, + const char *vcc_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *baseline_url; + const char *bc_url; + + /* Fetching DAV:checked-in from the VCC (with no Label: to specify a + revision) will return the latest Baseline resource's URL. */ + SVN_ERR(svn_ra_serf__fetch_dav_prop(&baseline_url, conn, vcc_url, + SVN_INVALID_REVNUM, + "checked-in", + scratch_pool, scratch_pool)); + if (!baseline_url) + { + return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("The OPTIONS response did not include " + "the requested checked-in value")); + } + baseline_url = svn_urlpath__canonicalize(baseline_url, scratch_pool); + + /* From the Baseline resource, we can fetch the DAV:baseline-collection + and DAV:version-name properties. The latter is the revision number, + which is formally the name used in Label: headers. */ + + /* First check baseline information cache. */ + SVN_ERR(svn_ra_serf__blncache_get_baseline_info(&bc_url, + youngest, + conn->session->blncache, + baseline_url, + scratch_pool)); + if (!bc_url) + { + SVN_ERR(retrieve_baseline_info(youngest, &bc_url, conn, + baseline_url, SVN_INVALID_REVNUM, + scratch_pool, scratch_pool)); + SVN_ERR(svn_ra_serf__blncache_set(conn->session->blncache, + baseline_url, *youngest, + bc_url, scratch_pool)); + } + + if (basecoll_url != NULL) + *basecoll_url = apr_pstrdup(result_pool, bc_url); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__get_youngest_revnum(svn_revnum_t *youngest, + svn_ra_serf__session_t *session, + apr_pool_t *scratch_pool) +{ + const char *vcc_url; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + return svn_error_trace(svn_ra_serf__v2_get_youngest_revnum( + youngest, session->conns[0], scratch_pool)); + + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, scratch_pool)); + + return svn_error_trace(v1_get_youngest_revnum(youngest, NULL, + session->conns[0], vcc_url, + scratch_pool, scratch_pool)); +} + + +/* Set *BC_URL to the baseline collection url for REVISION. If REVISION + is SVN_INVALID_REVNUM, then the youngest revnum ("HEAD") is used. + + *REVNUM_USED will be set to the revision used. + + Uses the specified CONN, which is part of SESSION. + + All allocations (results and temporary) are performed in POOL. */ +static svn_error_t * +get_baseline_info(const char **bc_url, + svn_revnum_t *revnum_used, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + svn_revnum_t revision, + apr_pool_t *pool) +{ + /* If we detected HTTP v2 support on the server, we can construct + the baseline collection URL ourselves, and fetch the latest + revision (if needed) with an OPTIONS request. */ + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + { + if (SVN_IS_VALID_REVNUM(revision)) + { + *revnum_used = revision; + } + else + { + SVN_ERR(svn_ra_serf__v2_get_youngest_revnum( + revnum_used, conn, pool)); + if (! SVN_IS_VALID_REVNUM(*revnum_used)) + return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("The OPTIONS response did not include " + "the youngest revision")); + } + + *bc_url = apr_psprintf(pool, "%s/%ld", + session->rev_root_stub, *revnum_used); + } + + /* Otherwise, we fall back to the old VCC_URL PROPFIND hunt. */ + else + { + const char *vcc_url; + + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, conn, pool)); + + if (SVN_IS_VALID_REVNUM(revision)) + { + /* First check baseline information cache. */ + SVN_ERR(svn_ra_serf__blncache_get_bc_url(bc_url, + session->blncache, + revision, pool)); + if (!*bc_url) + { + SVN_ERR(retrieve_baseline_info(NULL, bc_url, conn, + vcc_url, revision, pool, pool)); + SVN_ERR(svn_ra_serf__blncache_set(session->blncache, NULL, + revision, *bc_url, pool)); + } + + *revnum_used = revision; + } + else + { + SVN_ERR(v1_get_youngest_revnum(revnum_used, bc_url, + conn, vcc_url, + pool, pool)); + } + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__get_stable_url(const char **stable_url, + svn_revnum_t *latest_revnum, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *basecoll_url; + const char *repos_relpath; + svn_revnum_t revnum_used; + + /* No URL? No sweat. We'll use the session URL. */ + if (! url) + url = session->session_url.path; + + /* If the caller didn't provide a specific connection for us to use, + we'll use the default connection. */ + if (! conn) + conn = session->conns[0]; + + SVN_ERR(get_baseline_info(&basecoll_url, &revnum_used, + session, conn, revision, scratch_pool)); + SVN_ERR(svn_ra_serf__get_relative_path(&repos_relpath, url, + session, conn, scratch_pool)); + + *stable_url = svn_path_url_add_component2(basecoll_url, repos_relpath, + result_pool); + if (latest_revnum) + *latest_revnum = revnum_used; + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__get_resource_type(svn_node_kind_t *kind, + apr_hash_t *props) +{ + apr_hash_t *dav_props; + const char *res_type; + + dav_props = apr_hash_get(props, "DAV:", 4); + res_type = svn_prop_get_value(dav_props, "resourcetype"); + if (!res_type) + { + /* How did this happen? */ + return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL, + _("The PROPFIND response did not include the " + "requested resourcetype value")); + } + + if (strcmp(res_type, "collection") == 0) + { + *kind = svn_node_dir; + } + else + { + *kind = svn_node_file; + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__fetch_dav_prop(const char **value, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + const char *propname, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + apr_hash_t *props; + apr_hash_t *dav_props; + + SVN_ERR(svn_ra_serf__fetch_node_props(&props, conn, url, revision, + checked_in_props, + scratch_pool, scratch_pool)); + dav_props = apr_hash_get(props, "DAV:", 4); + if (dav_props == NULL) + return svn_error_create(SVN_ERR_RA_DAV_PROPS_NOT_FOUND, NULL, + _("The PROPFIND response did not include " + "the requested 'DAV:' properties")); + + /* We wouldn't get here if the resource was not found (404), so the + property should be present. + + Note: it is okay to call apr_pstrdup() with NULL. */ + *value = apr_pstrdup(result_pool, svn_prop_get_value(dav_props, propname)); + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/ra_serf.h b/subversion/libsvn_ra_serf/ra_serf.h new file mode 100644 index 0000000..3f3f3de --- /dev/null +++ b/subversion/libsvn_ra_serf/ra_serf.h @@ -0,0 +1,1785 @@ +/* + * ra_serf.h : Private declarations for the Serf-based DAV RA module. + * + * ==================================================================== + * 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_LIBSVN_RA_SERF_RA_SERF_H +#define SVN_LIBSVN_RA_SERF_RA_SERF_H + + +#include <serf.h> +#include <expat.h> /* for XML_Parser */ +#include <apr_uri.h> + +#include "svn_types.h" +#include "svn_string.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_delta.h" +#include "svn_version.h" +#include "svn_dav.h" +#include "svn_dirent_uri.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_subr_private.h" +#include "private/svn_editor.h" + +#include "blncache.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +/* Enforce the minimum version of serf. */ +#if !SERF_VERSION_AT_LEAST(1, 2, 1) +#error Please update your version of serf to at least 1.2.1. +#endif + +/** Use this to silence compiler warnings about unused parameters. */ +#define UNUSED_CTX(x) ((void)(x)) + +/** Our User-Agent string. */ +#define USER_AGENT "SVN/" SVN_VER_NUMBER " (" SVN_BUILD_TARGET ")" \ + " serf/" \ + APR_STRINGIFY(SERF_MAJOR_VERSION) "." \ + APR_STRINGIFY(SERF_MINOR_VERSION) "." \ + APR_STRINGIFY(SERF_PATCH_VERSION) + +/** Wait duration (in microseconds) used in calls to serf_context_run() */ +#define SVN_RA_SERF__CONTEXT_RUN_DURATION 500000 + + + +/* Forward declarations. */ +typedef struct svn_ra_serf__session_t svn_ra_serf__session_t; + +/* A serf connection and optionally associated SSL context. */ +typedef struct svn_ra_serf__connection_t { + /* Our connection to a server. */ + serf_connection_t *conn; + + /* Bucket allocator for this connection. */ + serf_bucket_alloc_t *bkt_alloc; + + /* Collected cert failures in chain. */ + int server_cert_failures; + + /* What was the last HTTP status code we got on this connection? */ + int last_status_code; + + /* Optional SSL context for this connection. */ + serf_ssl_context_t *ssl_context; + svn_auth_iterstate_t *ssl_client_auth_state; + svn_auth_iterstate_t *ssl_client_pw_auth_state; + + svn_ra_serf__session_t *session; + +} svn_ra_serf__connection_t; + +/** Maximum value we'll allow for the http-max-connections config option. + * + * Note: minimum 2 connections are required for ra_serf to function + * correctly! + */ +#define SVN_RA_SERF__MAX_CONNECTIONS_LIMIT 8 + +/* + * The master serf RA session. + * + * This is stored in the ra session ->priv field. + */ +struct svn_ra_serf__session_t { + /* Pool for allocations during this session */ + apr_pool_t *pool; + + /* The current context */ + serf_context_t *context; + + /* The maximum number of connections we'll use for parallelized + fetch operations (updates, etc.) */ + apr_int64_t max_connections; + + /* Are we using ssl */ + svn_boolean_t using_ssl; + + /* Should we ask for compressed responses? */ + svn_boolean_t using_compression; + + /* The user agent string */ + const char *useragent; + + /* The current connection */ + svn_ra_serf__connection_t *conns[SVN_RA_SERF__MAX_CONNECTIONS_LIMIT]; + int num_conns; + int cur_conn; + + /* The URL that was passed into _open() */ + apr_uri_t session_url; + const char *session_url_str; + + /* The actual discovered root; may be NULL until we know it. */ + apr_uri_t repos_root; + const char *repos_root_str; + + /* The server is not Apache/mod_dav_svn (directly) and only supports + HTTP/1.0. Thus, we cannot send chunked requests. */ + svn_boolean_t http10; + + /* Our Version-Controlled-Configuration; may be NULL until we know it. */ + const char *vcc_url; + + /* Authentication related properties. */ + svn_auth_iterstate_t *auth_state; + int auth_attempts; + + /* Callback functions to get info from WC */ + const svn_ra_callbacks2_t *wc_callbacks; + void *wc_callback_baton; + + /* Callback function to send progress info to the client */ + svn_ra_progress_notify_func_t progress_func; + void *progress_baton; + + /* Callback function to handle cancellation */ + svn_cancel_func_t cancel_func; + void *cancel_baton; + + /* Ev2 shim callbacks */ + svn_delta_shim_callbacks_t *shim_callbacks; + + /* Error that we've received but not yet returned upstream. */ + svn_error_t *pending_error; + + /* List of authn types supported by the client.*/ + int authn_types; + + /* Maps SVN_RA_CAPABILITY_foo keys to "yes" or "no" values. + If a capability is not yet discovered, it is absent from the table. + The table itself is allocated in the svn_ra_serf__session_t's pool; + keys and values must have at least that lifetime. Most likely + the keys and values are constants anyway (and sufficiently + well-informed internal code may just compare against those + constants' addresses, therefore). */ + apr_hash_t *capabilities; + + /* Activity collection URL. (Cached from the initial OPTIONS + request when run against HTTPv1 servers.) */ + const char *activity_collection_url; + + /* Are we using a proxy? */ + int using_proxy; + + const char *proxy_username; + const char *proxy_password; + int proxy_auth_attempts; + + /* SSL server certificates */ + svn_boolean_t trust_default_ca; + const char *ssl_authorities; + + /* Repository UUID */ + const char *uuid; + + /* Connection timeout value */ + apr_interval_time_t timeout; + + /* HTTPv1 flags */ + svn_tristate_t supports_deadprop_count; + + /*** HTTP v2 protocol stuff. *** + * + * We assume that if mod_dav_svn sends one of the special v2 OPTIONs + * response headers, it has sent all of them. Specifically, we'll + * be looking at the presence of the "me resource" as a flag that + * the server supports v2 of our HTTP protocol. + */ + + /* The "me resource". Typically used as a target for REPORTs that + are path-agnostic. If we have this, we can speak HTTP v2 to the + server. */ + const char *me_resource; + + /* Opaque URL "stubs". If the OPTIONS response returns these, then + we know we're using HTTP protocol v2. */ + const char *rev_stub; /* for accessing revisions (i.e. revprops) */ + const char *rev_root_stub; /* for accessing REV/PATH pairs */ + const char *txn_stub; /* for accessing transactions (i.e. txnprops) */ + const char *txn_root_stub; /* for accessing TXN/PATH pairs */ + const char *vtxn_stub; /* for accessing transactions (i.e. txnprops) */ + const char *vtxn_root_stub; /* for accessing TXN/PATH pairs */ + + /* Hash mapping const char * server-supported POST types to + disinteresting-but-non-null values. */ + apr_hash_t *supported_posts; + + /*** End HTTP v2 stuff ***/ + + svn_ra_serf__blncache_t *blncache; + + /* Trisate flag that indicates user preference for using bulk updates + (svn_tristate_true) with all the properties and content in the + update-report response. If svn_tristate_false, request a skelta + update-report with inlined properties. If svn_tristate_unknown then use + server preference. */ + svn_tristate_t bulk_updates; + + /* Indicates if the server wants bulk update requests (Prefer) or only + accepts skelta requests (Off). If this value is On both options are + allowed. */ + const char *server_allows_bulk; + + /* Indicates if the server supports sending inlined props in update editor + * in skelta mode (send-all == 'false'). */ + svn_boolean_t supports_inline_props; + + /* Indicates whether the server supports issuing replay REPORTs + against rev resources (children of `rev_stub', elsestruct). */ + svn_boolean_t supports_rev_rsrc_replay; +}; + +#define SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(sess) ((sess)->me_resource != NULL) + +/* + * Structure which represents a DAV element with a NAMESPACE and NAME. + */ +typedef struct svn_ra_serf__dav_props_t { + /* Element namespace */ + const char *namespace; + /* Element name */ + const char *name; +} svn_ra_serf__dav_props_t; + +/* + * Structure which represents an XML namespace. + */ +typedef struct ns_t { + /* The assigned name. */ + const char *namespace; + /* The full URL for this namespace. */ + const char *url; + /* The next namespace in our list. */ + struct ns_t *next; +} svn_ra_serf__ns_t; + +/* + * An incredibly simple list. + */ +typedef struct ra_serf_list_t { + void *data; + struct ra_serf_list_t *next; +} svn_ra_serf__list_t; + +/** DAV property sets **/ + +static const svn_ra_serf__dav_props_t base_props[] = +{ + { "DAV:", "version-controlled-configuration" }, + { "DAV:", "resourcetype" }, + { SVN_DAV_PROP_NS_DAV, "baseline-relative-path" }, + { SVN_DAV_PROP_NS_DAV, "repository-uuid" }, + { NULL } +}; + +static const svn_ra_serf__dav_props_t checked_in_props[] = +{ + { "DAV:", "checked-in" }, + { NULL } +}; + +static const svn_ra_serf__dav_props_t baseline_props[] = +{ + { "DAV:", "baseline-collection" }, + { "DAV:", SVN_DAV__VERSION_NAME }, + { NULL } +}; + +static const svn_ra_serf__dav_props_t all_props[] = +{ + { "DAV:", "allprop" }, + { NULL } +}; + +static const svn_ra_serf__dav_props_t check_path_props[] = +{ + { "DAV:", "resourcetype" }, + { NULL } +}; + +static const svn_ra_serf__dav_props_t type_and_checksum_props[] = +{ + { "DAV:", "resourcetype" }, + { SVN_DAV_PROP_NS_DAV, "sha1-checksum" }, + { NULL } +}; + +/* WC props compatibility with ra_neon. */ +#define SVN_RA_SERF__WC_CHECKED_IN_URL SVN_PROP_WC_PREFIX "ra_dav:version-url" + +/** Serf utility functions **/ + +apr_status_t +svn_ra_serf__conn_setup(apr_socket_t *sock, + serf_bucket_t **read_bkt, + serf_bucket_t **write_bkt, + void *baton, + apr_pool_t *pool); + +void +svn_ra_serf__conn_closed(serf_connection_t *conn, + void *closed_baton, + apr_status_t why, + apr_pool_t *pool); + + +/* Helper function to provide SSL client certificates. + * + * NOTE: This function sets the session's 'pending_error' member when + * returning an non-success status. + */ +apr_status_t +svn_ra_serf__handle_client_cert(void *data, + const char **cert_path); + +/* Helper function to provide SSL client certificate passwords. + * + * NOTE: This function sets the session's 'pending_error' member when + * returning an non-success status. + */ +apr_status_t +svn_ra_serf__handle_client_cert_pw(void *data, + const char *cert_path, + const char **password); + + +/* + * This function will run the serf context in SESS until *DONE is TRUE. + */ +svn_error_t * +svn_ra_serf__context_run_wait(svn_boolean_t *done, + svn_ra_serf__session_t *sess, + apr_pool_t *scratch_pool); + +/* Callback for response handlers */ +typedef svn_error_t * +(*svn_ra_serf__response_handler_t)(serf_request_t *request, + serf_bucket_t *response, + void *handler_baton, + apr_pool_t *scratch_pool); + +/* Callback for when a request body is needed. */ +/* ### should pass a scratch_pool */ +typedef svn_error_t * +(*svn_ra_serf__request_body_delegate_t)(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *request_pool); + +/* Callback for when request headers are needed. */ +/* ### should pass a scratch_pool */ +typedef svn_error_t * +(*svn_ra_serf__request_header_delegate_t)(serf_bucket_t *headers, + void *baton, + apr_pool_t *request_pool); + +/* Callback for when a response has an error. */ +typedef svn_error_t * +(*svn_ra_serf__response_error_t)(serf_request_t *request, + serf_bucket_t *response, + int status_code, + void *baton); + +/* ### we should reorder the types in this file. */ +typedef struct svn_ra_serf__server_error_t svn_ra_serf__server_error_t; + +/* + * Structure that can be passed to our default handler to guide the + * execution of the request through its lifecycle. + */ +typedef struct svn_ra_serf__handler_t { + /* The HTTP method string of the request */ + const char *method; + + /* The resource to the execute the method on. */ + const char *path; + + /* The content-type of the request body. */ + const char *body_type; + + /* If TRUE then default Accept-Encoding request header is not configured for + request. If FALSE then 'gzip' accept encoding will be used if compression + enabled. */ + svn_boolean_t custom_accept_encoding; + + /* Has the request/response been completed? */ + svn_boolean_t done; + + /* If we captured an error from the server, then this will be non-NULL. + It will be allocated from HANDLER_POOL. */ + svn_ra_serf__server_error_t *server_error; + + /* The handler and baton pair for our handler. */ + svn_ra_serf__response_handler_t response_handler; + void *response_baton; + + /* When REPONSE_HANDLER is invoked, the following fields will be set + based on the response header. HANDLER_POOL must be non-NULL for these + values to be filled in. SLINE.REASON and LOCATION will be allocated + within HANDLER_POOL. */ + serf_status_line sline; /* The parsed Status-Line */ + const char *location; /* The Location: header, if any */ + + /* The handler and baton pair to be executed when a non-recoverable error + * is detected. If it is NULL in the presence of an error, an abort() may + * be triggered. + */ + svn_ra_serf__response_error_t response_error; + void *response_error_baton; + + /* This function and baton pair allows for custom request headers to + * be set. + * + * It will be executed after the request has been set up but before it is + * delivered. + */ + svn_ra_serf__request_header_delegate_t header_delegate; + void *header_delegate_baton; + + /* This function and baton pair allows a body to be created right before + * delivery. + * + * It will be executed after the request has been set up but before it is + * delivered. + * + * May be NULL if there is no body to send. + * + */ + svn_ra_serf__request_body_delegate_t body_delegate; + void *body_delegate_baton; + + /* The connection and session to be used for this request. */ + svn_ra_serf__connection_t *conn; + svn_ra_serf__session_t *session; + + /* Internal flag to indicate we've parsed the headers. */ + svn_boolean_t reading_body; + + /* When this flag will be set, the core handler will discard any unread + portion of the response body. The registered response handler will + no longer be called. */ + svn_boolean_t discard_body; + + /* Pool for allocating SLINE.REASON and LOCATION. If this pool is NULL, + then the requestor does not care about SLINE and LOCATION. */ + apr_pool_t *handler_pool; + +} svn_ra_serf__handler_t; + + +/* Run one request and process the response. + + Similar to context_run_wait(), but this creates the request for HANDLER + and then waits for it to complete. + + WARNING: context_run_wait() does NOT create a request, whereas this + function DOES. Avoid a double-create. */ +svn_error_t * +svn_ra_serf__context_run_one(svn_ra_serf__handler_t *handler, + apr_pool_t *scratch_pool); + + +/* + * Helper function to queue a request in the @a handler's connection. + */ +void svn_ra_serf__request_create(svn_ra_serf__handler_t *handler); + +/* XML helper callbacks. */ + +typedef struct svn_ra_serf__xml_state_t { + /* A numeric value that represents the current state in parsing. + * + * Value 0 is reserved for use as the default state. + */ + int current_state; + + /* Private pointer set by the parsing code. */ + void *private; + + /* Allocations should be made in this pool to match the lifetime of the + * state. + */ + apr_pool_t *pool; + + /* The currently-declared namespace for this state. */ + svn_ra_serf__ns_t *ns_list; + + /* Our previous states. */ + struct svn_ra_serf__xml_state_t *prev; +} svn_ra_serf__xml_state_t; + +/* Forward declaration of the XML parser structure. */ +typedef struct svn_ra_serf__xml_parser_t svn_ra_serf__xml_parser_t; + +/* Callback invoked with @a baton by our XML @a parser when an element with + * the @a name containing @a attrs is opened. + */ +typedef svn_error_t * +(*svn_ra_serf__xml_start_element_t)(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool); + +/* Callback invoked with @a baton by our XML @a parser when an element with + * the @a name is closed. + */ +typedef svn_error_t * +(*svn_ra_serf__xml_end_element_t)(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool); + +/* Callback invoked with @a baton by our XML @a parser when a CDATA portion + * of @a data with size @a len is encountered. + * + * This may be invoked multiple times for the same tag. + */ +typedef svn_error_t * +(*svn_ra_serf__xml_cdata_chunk_handler_t)(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool); + +/* + * Helper structure associated with handle_xml_parser handler that will + * specify how an XML response will be processed. + */ +struct svn_ra_serf__xml_parser_t { + /* Temporary allocations should be made in this pool. */ + apr_pool_t *pool; + + /* What kind of response are we parsing? If set, this should typically + define the report name. */ + const char *response_type; + + /* Caller-specific data passed to the start, end, cdata callbacks. */ + void *user_data; + + /* Callback invoked when a tag is opened. */ + svn_ra_serf__xml_start_element_t start; + + /* Callback invoked when a tag is closed. */ + svn_ra_serf__xml_end_element_t end; + + /* Callback invoked when a cdata chunk is received. */ + svn_ra_serf__xml_cdata_chunk_handler_t cdata; + + /* Our associated expat-based XML parser. */ + XML_Parser xmlp; + + /* Our current state. */ + svn_ra_serf__xml_state_t *state; + + /* Our previously used states (will be reused). */ + svn_ra_serf__xml_state_t *free_state; + + /* If non-NULL, this value will be set to TRUE when the response is + * completed. + */ + svn_boolean_t *done; + + /* If non-NULL, when this parser completes, it will add done_item to + * the list. + */ + svn_ra_serf__list_t **done_list; + + /* A pointer to the item that will be inserted into the list upon + * completeion. + */ + svn_ra_serf__list_t *done_item; + + /* If this flag is TRUE, errors during parsing will be ignored. + * + * This is mainly used when we are processing an error XML response to + * avoid infinite loops. + */ + svn_boolean_t ignore_errors; + + /* If an error occurred, this value will be non-NULL. */ + svn_error_t *error; + + /* Deciding whether to pause, or not, is performed within the parsing + callbacks. If a callback decides to set this flag, then the loop + driving the parse (generally, a series of calls to serf_context_run()) + is going to need to coordinate the un-pausing of the parser by + processing pending content. Thus, deciding to pause the parser is a + coordinate effort rather than merely setting this flag. + + When an XML parsing callback sets this flag, note that additional + elements may be parsed (as the current buffer is consumed). At some + point, the flag will be recognized and arriving network content will + be stashed away in the PENDING structure (see below). + + At some point, the controlling loop should clear this value. The + underlying network processing will note the change and begin passing + content into the XML callbacks. + + Note that the controlling loop should also process pending content + since the arriving network content will typically finish first. */ + svn_boolean_t paused; + + /* While the XML parser is paused, content arriving from the server + must be saved locally. We cannot stop reading, or the server may + decide to drop the connection. The content will be stored in memory + up to a certain limit, and will then be spilled over to disk. + + See libsvn_ra_serf/util.c */ + struct svn_ra_serf__pending_t *pending; + + /* Response restart support */ + const void *headers_baton; /* Last pointer to headers */ + apr_off_t skip_size; /* Number of bytes to skip */ + apr_off_t read_size; /* Number of bytes read from response */ +}; + + +/* v2 of the XML parsing functions */ + +/* The XML parsing context. */ +typedef struct svn_ra_serf__xml_context_t svn_ra_serf__xml_context_t; + + +/* An opaque structure for the XML parse element/state. */ +typedef struct svn_ra_serf__xml_estate_t svn_ra_serf__xml_estate_t; + +/* Called just after the parser moves into ENTERED_STATE. The tag causing + the transition is passed in TAG. + + This callback is applied to a parsing context by using the + svn_ra_serf__xml_context_customize() function. + + NOTE: this callback, when set, will be invoked on *every* transition. + The callback must examine ENTERED_STATE to determine if any action + must be taken. The original state is not provided, but must be derived + from ENTERED_STATE and/or the TAG causing the transition (if needed). */ +typedef svn_error_t * +(*svn_ra_serf__xml_opened_t)(svn_ra_serf__xml_estate_t *xes, + void *baton, + int entered_state, + const svn_ra_serf__dav_props_t *tag, + apr_pool_t *scratch_pool); + + +/* Called just before the parser leaves LEAVING_STATE. + + If cdata collection was enabled for this state, then CDATA will be + non-NULL and contain the collected cdata. + + If attribute collection was enabled for this state, then ATTRS will + contain the attributes collected for this element only, along with + any values stored via svn_ra_serf__xml_note(). + + Use svn_ra_serf__xml_gather_since() to gather up data from outer states. + + ATTRS is char* -> char*. + + Temporary allocations may be made in SCRATCH_POOL. */ +typedef svn_error_t * +(*svn_ra_serf__xml_closed_t)(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool); + + +/* Called for all states that are not using the builtin cdata collection. + This callback is (only) appropriate for unbounded-size cdata content. + + CURRENT_STATE may be used to decide what to do with the data. + + Temporary allocations may be made in SCRATCH_POOL. */ +typedef svn_error_t * +(*svn_ra_serf__xml_cdata_t)(svn_ra_serf__xml_estate_t *xes, + void *baton, + int current_state, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool); + + +/* State transition table. + + When the XML Context is constructed, it is in state 0. User states are + positive integers. + + In a list of transitions, use { 0 } to indicate the end. Specifically, + the code looks for NS == NULL. + + ### more docco +*/ +typedef struct svn_ra_serf__xml_transition_t { + /* This transition applies when in this state */ + int from_state; + + /* And when this tag is observed */ + const char *ns; + const char *name; + + /* Moving to this state */ + int to_state; + + /* Should the cdata of NAME be collected? Note that CUSTOM_CLOSE should + be TRUE in order to capture this cdata. */ + svn_boolean_t collect_cdata; + + /* Which attributes of NAME should be collected? Terminate with NULL. + Maximum of 10 attributes may be collected. Note that attribute + namespaces are ignored at this time. + + Attribute names beginning with "?" are optional. Other names must + exist on the element, or SVN_ERR_XML_ATTRIB_NOT_FOUND will be raised. */ + const char *collect_attrs[11]; + + /* When NAME is closed, should the callback be invoked? */ + svn_boolean_t custom_close; + +} svn_ra_serf__xml_transition_t; + + +/* Construct an XML parsing context, based on the TTABLE transition table. + As content is parsed, the CLOSED_CB callback will be invoked according + to the definition in the table. + + If OPENED_CB is not NULL, then it will be invoked for *every* tag-open + event. The callback will need to use the ENTERED_STATE and TAG parameters + to decide what it would like to do. + + If CDATA_CB is not NULL, then it will be called for all cdata that is + not be automatically collected (based on the transition table record's + COLLECT_CDATA flag). It will be called in every state, so the callback + must examine the CURRENT_STATE parameter to decide what to do. + + The same BATON value will be passed to all three callbacks. + + The context will be created within RESULT_POOL. */ +svn_ra_serf__xml_context_t * +svn_ra_serf__xml_context_create( + const svn_ra_serf__xml_transition_t *ttable, + svn_ra_serf__xml_opened_t opened_cb, + svn_ra_serf__xml_closed_t closed_cb, + svn_ra_serf__xml_cdata_t cdata_cb, + void *baton, + apr_pool_t *result_pool); + +/* Destroy all subpools for this structure. */ +void +svn_ra_serf__xml_context_destroy( + svn_ra_serf__xml_context_t *xmlctx); + +/* Construct a handler with the response function/baton set up to parse + a response body using the given XML context. The handler and its + internal structures are allocated in RESULT_POOL. + + This also initializes HANDLER_POOL to the given RESULT_POOL. */ +svn_ra_serf__handler_t * +svn_ra_serf__create_expat_handler(svn_ra_serf__xml_context_t *xmlctx, + apr_pool_t *result_pool); + + +/* Allocated within XES->STATE_POOL. Changes are not allowd (callers + should make a deep copy if they need to make changes). + + The resulting hash maps char* names to char* values. */ +apr_hash_t * +svn_ra_serf__xml_gather_since(svn_ra_serf__xml_estate_t *xes, + int stop_state); + + +/* Attach the NAME/VALUE pair onto this/parent state identified by STATE. + The name and value will be copied into the target state's pool. + + These values will be available to the CLOSED_CB for the target state, + or part of the gathered state via xml_gather_since(). + + Typically, this function is used by a child state's close callback, + or within an opening callback to store additional data. + + Note: if the state is not found, then a programmer error has occurred, + so the function will invoke SVN_ERR_MALFUNCTION(). */ +void +svn_ra_serf__xml_note(svn_ra_serf__xml_estate_t *xes, + int state, + const char *name, + const char *value); + + +/* Returns XES->STATE_POOL for allocating structures that should live + as long as the state identified by XES. + + Note: a state pool is created upon demand, so only use this function + when memory is required for a given state. */ +apr_pool_t * +svn_ra_serf__xml_state_pool(svn_ra_serf__xml_estate_t *xes); + + +/* Any XML parser may be used. When an opening tag is seen, call this + function to feed the information into XMLCTX. */ +svn_error_t * +svn_ra_serf__xml_cb_start(svn_ra_serf__xml_context_t *xmlctx, + const char *raw_name, + const char *const *attrs); + + +/* When a close tag is seen, call this function to feed the information + into XMLCTX. */ +svn_error_t * +svn_ra_serf__xml_cb_end(svn_ra_serf__xml_context_t *xmlctx, + const char *raw_name); + + +/* When cdata is parsed by the wrapping XML parser, call this function to + feed the cdata into the XMLCTX. */ +svn_error_t * +svn_ra_serf__xml_cb_cdata(svn_ra_serf__xml_context_t *xmlctx, + const char *data, + apr_size_t len); + + +/* + * Parses a server-side error message into a local Subversion error. + */ +struct svn_ra_serf__server_error_t { + /* Our local representation of the error. */ + svn_error_t *error; + + /* Are we done with the response? */ + svn_boolean_t done; + + /* Have we seen an error tag? */ + svn_boolean_t in_error; + + /* Have we seen a HTTP "412 Precondition Failed" error? */ + svn_boolean_t contains_precondition_error; + + /* Should we be collecting the XML cdata? */ + svn_boolean_t collect_cdata; + + /* Collected cdata. NULL if cdata not needed. */ + svn_stringbuf_t *cdata; + + /* XML parser and namespace used to parse the remote response */ + svn_ra_serf__xml_parser_t parser; +}; + + +/* + * Handler that discards the entire @a response body associated with a + * @a request. Implements svn_ra_serf__response_handler_t. + * + * If @a baton is a svn_ra_serf__server_error_t (i.e. non-NULL) and an + * error is detected, it will be populated for later detection. + * + * All temporary allocations will be made in a @a pool. + */ +svn_error_t * +svn_ra_serf__handle_discard_body(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool); + + +/* + * Handler that retrieves the embedded XML multistatus response from the + * the @a RESPONSE body associated with a @a REQUEST. + * + * Implements svn_ra_serf__response_handler_t. + * + * The @a BATON should be of type svn_ra_serf__handler_t. When the request + * is complete, the handler's DONE flag will be set to TRUE. + * + * All temporary allocations will be made in a @a scratch_pool. + */ +svn_error_t * +svn_ra_serf__handle_multistatus_only(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool); + + +/* Handler that expects an empty body. + + If a body IS present, and it is text/xml, then it will be parsed for + a server-side error. + + BATON should be the svn_ra_serf__handler_t running REQUEST. + + Status line information will be in HANDLER->SLINE. + + Any parsed errors will be left in HANDLER->SERVER_ERROR. That member + may be NULL if no body was present, or a problem occurred trying to + parse the body. + + All temporary allocations will be made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__expect_empty_body(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool); + + +/* + * This function will feed the RESPONSE body into XMLP. When parsing is + * completed (i.e. an EOF is received), *DONE is set to TRUE. + * Implements svn_ra_serf__response_handler_t. + * + * If an error occurs during processing RESP_ERR is invoked with the + * RESP_ERR_BATON. + * + * Temporary allocations are made in POOL. + */ +svn_error_t * +svn_ra_serf__handle_xml_parser(serf_request_t *request, + serf_bucket_t *response, + void *handler_baton, + apr_pool_t *pool); + +/* serf_response_handler_t implementation that completely discards + * the response. + * + * All temporary allocations will be made in @a pool. + */ +apr_status_t +svn_ra_serf__response_discard_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool); + + +/** XML helper functions. **/ + +/* + * Advance the internal XML @a parser to the @a state. + */ +void +svn_ra_serf__xml_push_state(svn_ra_serf__xml_parser_t *parser, + int state); + +/* + * Return to the previous internal XML @a parser state. + */ +void +svn_ra_serf__xml_pop_state(svn_ra_serf__xml_parser_t *parser); + + +svn_error_t * +svn_ra_serf__process_pending(svn_ra_serf__xml_parser_t *parser, + svn_boolean_t *network_eof, + apr_pool_t *scratch_pool); + + +/* + * Add the appropriate serf buckets to @a agg_bucket represented by + * the XML * @a tag and @a value. + * + * The bucket will be allocated from @a bkt_alloc. + */ +void +svn_ra_serf__add_tag_buckets(serf_bucket_t *agg_bucket, + const char *tag, + const char *value, + serf_bucket_alloc_t *bkt_alloc); + +/* + * Add the appropriate serf buckets to AGG_BUCKET with standard XML header: + * <?xml version="1.0" encoding="utf-8"?> + * + * The bucket will be allocated from BKT_ALLOC. + */ +void +svn_ra_serf__add_xml_header_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc); + +/* + * Add the appropriate serf buckets to AGG_BUCKET representing the XML + * open tag with name TAG. + * + * Take the tag's attributes from varargs, a NULL-terminated list of + * alternating <tt>char *</tt> key and <tt>char *</tt> val. Attribute + * will be ignored if it's value is NULL. + * + * NOTE: Callers are responsible for XML-escaping attribute values as + * necessary. + * + * The bucket will be allocated from BKT_ALLOC. + */ +void +svn_ra_serf__add_open_tag_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *tag, + ...); + +/* + * Add the appropriate serf buckets to AGG_BUCKET representing xml tag close + * with name TAG. + * + * The bucket will be allocated from BKT_ALLOC. + */ +void +svn_ra_serf__add_close_tag_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *tag); + +/* + * Add the appropriate serf buckets to AGG_BUCKET with xml-escaped + * version of DATA. + * + * The bucket will be allocated from BKT_ALLOC. + */ +void +svn_ra_serf__add_cdata_len_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *data, apr_size_t len); +/* + * Look up the @a attrs array for namespace definitions and add each one + * to the @a ns_list of namespaces. + * + * New namespaces will be allocated in RESULT_POOL. + */ +void +svn_ra_serf__define_ns(svn_ra_serf__ns_t **ns_list, + const char *const *attrs, + apr_pool_t *result_pool); + +/* + * Look up @a name in the @a ns_list list for previously declared namespace + * definitions. + * + * Return (in @a *returned_prop_name) a #svn_ra_serf__dav_props_t tuple + * representing the expanded name. + */ +void +svn_ra_serf__expand_ns(svn_ra_serf__dav_props_t *returned_prop_name, + const svn_ra_serf__ns_t *ns_list, + const char *name); + + +/** PROPFIND-related functions **/ + +/* + * This function will deliver a PROP_CTX PROPFIND request in the SESS + * serf context for the properties listed in LOOKUP_PROPS at URL for + * DEPTH ("0","1","infinity"). + * + * This function will not block waiting for the response. Callers are + * expected to call svn_ra_serf__wait_for_props(). + */ +svn_error_t * +svn_ra_serf__deliver_props(svn_ra_serf__handler_t **propfind_handler, + apr_hash_t *prop_vals, + svn_ra_serf__session_t *sess, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t rev, + const char *depth, + const svn_ra_serf__dav_props_t *lookup_props, + svn_ra_serf__list_t **done_list, + apr_pool_t *pool); + +/* + * This helper function will block until PROPFIND_HANDLER indicates that is + * done or another error is returned. + */ +svn_error_t * +svn_ra_serf__wait_for_props(svn_ra_serf__handler_t *handler, + apr_pool_t *scratch_pool); + +/* This is a blocking version of deliver_props. + + The properties are fetched and placed into RESULTS, allocated in + RESULT_POOL. + + ### more docco about the other params. + + Temporary allocations are made in SCRATCH_POOL. +*/ +svn_error_t * +svn_ra_serf__retrieve_props(apr_hash_t **results, + svn_ra_serf__session_t *sess, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t rev, + const char *depth, + const svn_ra_serf__dav_props_t *props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Using CONN, fetch the properties specified by WHICH_PROPS using CONN + for URL at REVISION. The resulting properties are placed into a 2-level + hash in RESULTS, mapping NAMESPACE -> hash<PROPNAME, PROPVALUE>, which + is allocated in RESULT_POOL. + + If REVISION is SVN_INVALID_REVNUM, then the properties are fetched + from HEAD for URL. + + This function performs the request synchronously. + + Temporary allocations are made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__fetch_node_props(apr_hash_t **results, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + const svn_ra_serf__dav_props_t *which_props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Using CONN, fetch a DAV: property from the resource identified by URL + within REVISION. The PROPNAME may be one of: + + "checked-in" + "href" + + The resulting value will be allocated in RESULT_POOL, and may be NULL + if the property does not exist (note: "href" always exists). + + This function performs the request synchronously. + + Temporary allocations are made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__fetch_dav_prop(const char **value, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + const char *propname, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Set PROPS for PATH at REV revision with a NS:NAME VAL. + * + * The POOL governs allocation. + */ +void +svn_ra_serf__set_ver_prop(apr_hash_t *props, + const char *path, svn_revnum_t rev, + const char *ns, const char *name, + const svn_string_t *val, apr_pool_t *pool); +#define svn_ra_serf__set_rev_prop svn_ra_serf__set_ver_prop + +/** Property walker functions **/ + +typedef svn_error_t * +(*svn_ra_serf__walker_visitor_t)(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *pool); + +svn_error_t * +svn_ra_serf__walk_all_props(apr_hash_t *props, + const char *name, + svn_revnum_t rev, + svn_ra_serf__walker_visitor_t walker, + void *baton, + apr_pool_t *pool); + + +/* Like walk_all_props(), but a 2-level hash. */ +svn_error_t * +svn_ra_serf__walk_node_props(apr_hash_t *props, + svn_ra_serf__walker_visitor_t walker, + void *baton, + apr_pool_t *scratch_pool); + + +typedef svn_error_t * +(*svn_ra_serf__path_rev_walker_t)(void *baton, + const char *path, apr_ssize_t path_len, + const char *ns, apr_ssize_t ns_len, + const char *name, apr_ssize_t name_len, + const svn_string_t *val, + apr_pool_t *pool); +svn_error_t * +svn_ra_serf__walk_all_paths(apr_hash_t *props, + svn_revnum_t rev, + svn_ra_serf__path_rev_walker_t walker, + void *baton, + apr_pool_t *pool); + + +/* Map a property name, as passed over the wire, into its corresponding + Subversion-internal name. The returned name will be a static value, + or allocated within RESULT_POOL. + + If the property should be ignored (eg. some DAV properties), then NULL + will be returned. */ +const char * +svn_ra_serf__svnname_from_wirename(const char *ns, + const char *name, + apr_pool_t *result_pool); + + +/* Select the basic revision properties from the set of "all" properties. + Return these in *REVPROPS, allocated from RESULT_POOL. */ +svn_error_t * +svn_ra_serf__select_revprops(apr_hash_t **revprops, + const char *name, + svn_revnum_t rev, + apr_hash_t *all_revprops, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* PROPS is nested hash tables mapping NS -> NAME -> VALUE. + This function takes the NS:NAME:VALUE hashes and flattens them into a set of + names to VALUE. The names are composed of NS:NAME, with specific + rewrite from wire names (DAV) to SVN names. This mapping is managed + by the svn_ra_serf__set_baton_props() function. + + FLAT_PROPS is allocated in RESULT_POOL. + ### right now, we do a shallow copy from PROPS to FLAT_PROPS. therefore, + ### the names and values in PROPS must be in the proper pool. + + Temporary allocations are made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__flatten_props(apr_hash_t **flat_props, + apr_hash_t *props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Return the property value for PATH at REV revision with a NS:NAME. + * PROPS is a four-level nested hash: (svn_revnum_t => char *path => + * char *ns => char *name => svn_string_t *). */ +const svn_string_t * +svn_ra_serf__get_ver_prop_string(apr_hash_t *props, + const char *path, svn_revnum_t rev, + const char *ns, const char *name); + +/* Same as svn_ra_serf__get_ver_prop_string(), but returns a C string. */ +const char * +svn_ra_serf__get_ver_prop(apr_hash_t *props, + const char *path, svn_revnum_t rev, + const char *ns, const char *name); + +/* Same as svn_ra_serf__get_ver_prop_string(), but for the unknown revision. */ +const svn_string_t * +svn_ra_serf__get_prop_string(apr_hash_t *props, + const char *path, + const char *ns, + const char *name); + +/* Same as svn_ra_serf__get_ver_prop(), but for the unknown revision. */ +const char * +svn_ra_serf__get_prop(apr_hash_t *props, + const char *path, + const char *ns, + const char *name); + +/* Same as svn_ra_serf__set_rev_prop(), but for the unknown revision. */ +void +svn_ra_serf__set_prop(apr_hash_t *props, const char *path, + const char *ns, const char *name, + const svn_string_t *val, apr_pool_t *pool); + +svn_error_t * +svn_ra_serf__get_resource_type(svn_node_kind_t *kind, + apr_hash_t *props); + + +/** MERGE-related functions **/ + +void +svn_ra_serf__merge_lock_token_list(apr_hash_t *lock_tokens, + const char *parent, + serf_bucket_t *body, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool); + +/* Create an MERGE request aimed at the SESSION url, requesting the + merge of the resource identified by MERGE_RESOURCE_URL. + LOCK_TOKENS is a hash mapping paths to lock tokens owned by the + client. If KEEP_LOCKS is set, instruct the server to not release + locks set on the paths included in this commit. */ +svn_error_t * +svn_ra_serf__run_merge(const svn_commit_info_t **commit_info, + int *response_code, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + const char *merge_resource_url, + apr_hash_t *lock_tokens, + svn_boolean_t keep_locks, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/** OPTIONS-related functions **/ + +/* On HTTPv2 connections, run an OPTIONS request over CONN to fetch the + current youngest revnum, returning it in *YOUNGEST. + + (the revnum is headers of the OPTIONS response) + + This function performs the request synchronously. + + All temporary allocations will be made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__v2_get_youngest_revnum(svn_revnum_t *youngest, + svn_ra_serf__connection_t *conn, + apr_pool_t *scratch_pool); + + +/* On HTTPv1 connections, run an OPTIONS request over CONN to fetch the + activity collection set and return it in *ACTIVITY_URL, allocated + from RESULT_POOL. + + (the activity-collection-set is in the body of the OPTIONS response) + + This function performs the request synchronously. + + All temporary allocations will be made in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__v1_get_activity_collection(const char **activity_url, + svn_ra_serf__connection_t *conn, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/* Set @a VCC_URL to the default VCC for our repository based on @a + * ORIG_PATH for the session @a SESSION, ensuring that the VCC URL and + * repository root URLs are cached in @a SESSION. Use @a CONN for any + * required network communications if it is non-NULL; otherwise use the + * default connection. + * + * All temporary allocations will be made in @a POOL. */ +svn_error_t * +svn_ra_serf__discover_vcc(const char **vcc_url, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool); + +/* Set @a REPORT_TARGET to the URI of the resource at which generic + * (path-agnostic) REPORTs should be aimed for @a SESSION. Use @a + * CONN for any required network communications if it is non-NULL; + * otherwise use the default connection. + * + * All temporary allocations will be made in @a POOL. + */ +svn_error_t * +svn_ra_serf__report_resource(const char **report_target, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool); + +/* Set @a REL_PATH to a path (not URI-encoded) relative to the root of + * the repository pointed to by @a SESSION, based on original path + * (URI-encoded) @a ORIG_PATH. Use @a CONN for any required network + * communications if it is non-NULL; otherwise use the default + * connection. Use POOL for allocations. */ +svn_error_t * +svn_ra_serf__get_relative_path(const char **rel_path, + const char *orig_path, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool); + + +/* Using the default connection in SESSION (conns[0]), get the youngest + revnum from the server, returning it in *YOUNGEST. + + This function operates synchronously. + + All temporary allocations are performed in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__get_youngest_revnum(svn_revnum_t *youngest, + svn_ra_serf__session_t *session, + apr_pool_t *scratch_pool); + + +/* Generate a revision-stable URL. + + The RA APIs all refer to user/public URLs that float along with the + youngest revision. In many cases, we do NOT want to work with that URL + since it can change from one moment to the next. Especially if we + attempt to operation against multiple floating URLs -- we could end up + referring to two separate revisions. + + The DAV RA provider(s) solve this by generating a URL that is specific + to a revision by using a URL into a "baseline collection". + + For a specified SESSION, with an optional CONN (if NULL, then the + session's default connection will be used; specifically SESSION->conns[0]), + generate a revision-stable URL for URL at REVISION. If REVISION is + SVN_INVALID_REVNUM, then the stable URL will refer to the youngest + revision at the time this function was called. + + If URL is NULL, then the session root will be used. + + The stable URL will be placed into *STABLE_URL, allocated from RESULT_POOL. + + If LATEST_REVNUM is not NULL, then the revision used will be placed into + *LATEST_REVNUM. That will be equal to youngest, or the given REVISION. + + This function operates synchronously, if any communication to the server + is required. Communication is needed if REVISION is SVN_INVALID_REVNUM + (to get the current youngest revnum), or if the specified REVISION is not + (yet) in our cache of baseline collections. + + All temporary allocations are performed in SCRATCH_POOL. */ +svn_error_t * +svn_ra_serf__get_stable_url(const char **stable_url, + svn_revnum_t *latest_revnum, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + const char *url, + svn_revnum_t revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + + +/** RA functions **/ + +/* Implements svn_ra__vtable_t.get_log(). */ +svn_error_t * +svn_ra_serf__get_log(svn_ra_session_t *session, + 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_log_entry_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_locations(). */ +svn_error_t * +svn_ra_serf__get_locations(svn_ra_session_t *session, + apr_hash_t **locations, + const char *path, + svn_revnum_t peg_revision, + const apr_array_header_t *location_revisions, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_location_segments(). */ +svn_error_t * +svn_ra_serf__get_location_segments(svn_ra_session_t *session, + const char *path, + svn_revnum_t peg_revision, + svn_revnum_t start_rev, + svn_revnum_t end_rev, + svn_location_segment_receiver_t receiver, + void *receiver_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.do_diff(). */ +svn_error_t * +svn_ra_serf__do_diff(svn_ra_session_t *session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision, + const char *diff_target, + svn_depth_t depth, + svn_boolean_t ignore_ancestry, + svn_boolean_t text_deltas, + const char *versus_url, + const svn_delta_editor_t *diff_editor, + void *diff_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.do_status(). */ +svn_error_t * +svn_ra_serf__do_status(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + const char *status_target, + svn_revnum_t revision, + svn_depth_t depth, + const svn_delta_editor_t *status_editor, + void *status_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.do_update(). */ +svn_error_t * +svn_ra_serf__do_update(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision_to_update_to, + const char *update_target, + svn_depth_t depth, + svn_boolean_t send_copyfrom_args, + svn_boolean_t ignore_ancestry, + const svn_delta_editor_t *update_editor, + void *update_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Implements svn_ra__vtable_t.do_switch(). */ +svn_error_t * +svn_ra_serf__do_switch(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision_to_switch_to, + const char *switch_target, + svn_depth_t depth, + const char *switch_url, + svn_boolean_t send_copyfrom_args, + svn_boolean_t ignore_ancestry, + const svn_delta_editor_t *switch_editor, + void *switch_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Implements svn_ra__vtable_t.get_file_revs(). */ +svn_error_t * +svn_ra_serf__get_file_revs(svn_ra_session_t *session, + const char *path, + svn_revnum_t start, + svn_revnum_t end, + svn_boolean_t include_merged_revisions, + svn_file_rev_handler_t handler, + void *handler_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_dated_revision(). */ +svn_error_t * +svn_ra_serf__get_dated_revision(svn_ra_session_t *session, + svn_revnum_t *revision, + apr_time_t tm, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_commit_editor(). */ +svn_error_t * +svn_ra_serf__get_commit_editor(svn_ra_session_t *session, + const svn_delta_editor_t **editor, + void **edit_baton, + apr_hash_t *revprop_table, + svn_commit_callback2_t callback, + void *callback_baton, + apr_hash_t *lock_tokens, + svn_boolean_t keep_locks, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_file(). */ +svn_error_t * +svn_ra_serf__get_file(svn_ra_session_t *session, + const char *path, + svn_revnum_t revision, + svn_stream_t *stream, + svn_revnum_t *fetched_rev, + apr_hash_t **props, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.change_rev_prop(). */ +svn_error_t * +svn_ra_serf__change_rev_prop(svn_ra_session_t *session, + svn_revnum_t rev, + const char *name, + const svn_string_t *const *old_value_p, + const svn_string_t *value, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.replay(). */ +svn_error_t * +svn_ra_serf__replay(svn_ra_session_t *ra_session, + svn_revnum_t revision, + svn_revnum_t low_water_mark, + svn_boolean_t text_deltas, + const svn_delta_editor_t *editor, + void *edit_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.replay_range(). */ +svn_error_t * +svn_ra_serf__replay_range(svn_ra_session_t *ra_session, + svn_revnum_t start_revision, + svn_revnum_t end_revision, + svn_revnum_t low_water_mark, + svn_boolean_t send_deltas, + svn_ra_replay_revstart_callback_t revstart_func, + svn_ra_replay_revfinish_callback_t revfinish_func, + void *replay_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.lock(). */ +svn_error_t * +svn_ra_serf__lock(svn_ra_session_t *ra_session, + apr_hash_t *path_revs, + const char *comment, + svn_boolean_t force, + svn_ra_lock_callback_t lock_func, + void *lock_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.unlock(). */ +svn_error_t * +svn_ra_serf__unlock(svn_ra_session_t *ra_session, + apr_hash_t *path_tokens, + svn_boolean_t force, + svn_ra_lock_callback_t lock_func, + void *lock_baton, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_lock(). */ +svn_error_t * +svn_ra_serf__get_lock(svn_ra_session_t *ra_session, + svn_lock_t **lock, + const char *path, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_locks(). */ +svn_error_t * +svn_ra_serf__get_locks(svn_ra_session_t *ra_session, + apr_hash_t **locks, + const char *path, + svn_depth_t depth, + apr_pool_t *pool); + +/* Request a mergeinfo-report from the URL attached to SESSION, + and fill in the MERGEINFO hash with the results. + + Implements svn_ra__vtable_t.get_mergeinfo(). + */ +svn_error_t * +svn_ra_serf__get_mergeinfo(svn_ra_session_t *ra_session, + apr_hash_t **mergeinfo, + const apr_array_header_t *paths, + svn_revnum_t revision, + svn_mergeinfo_inheritance_t inherit, + svn_boolean_t include_descendants, + apr_pool_t *pool); + +/* Exchange capabilities with the server, by sending an OPTIONS + * request announcing the client's capabilities, and by filling + * SERF_SESS->capabilities with the server's capabilities as read from + * the response headers. Use POOL only for temporary allocation. + * + * If the CORRECTED_URL is non-NULL, allow the OPTIONS response to + * report a server-dictated redirect or relocation (HTTP 301 or 302 + * error codes), setting *CORRECTED_URL to the value of the corrected + * repository URL. Otherwise, such responses from the server will + * generate an error. (In either case, no capabilities are exchanged + * if there is, in fact, such a response from the server.) + */ +svn_error_t * +svn_ra_serf__exchange_capabilities(svn_ra_serf__session_t *serf_sess, + const char **corrected_url, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.has_capability(). */ +svn_error_t * +svn_ra_serf__has_capability(svn_ra_session_t *ra_session, + svn_boolean_t *has, + const char *capability, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.get_deleted_rev(). */ +svn_error_t * +svn_ra_serf__get_deleted_rev(svn_ra_session_t *session, + const char *path, + svn_revnum_t peg_revision, + svn_revnum_t end_revision, + svn_revnum_t *revision_deleted, + apr_pool_t *pool); + +/* Implements the get_inherited_props RA layer function. */ +svn_error_t * svn_ra_serf__get_inherited_props(svn_ra_session_t *session, + apr_array_header_t **iprops, + const char *path, + svn_revnum_t revision, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* Implements svn_ra__vtable_t.get_repos_root(). */ +svn_error_t * +svn_ra_serf__get_repos_root(svn_ra_session_t *ra_session, + const char **url, + apr_pool_t *pool); + +/* Implements svn_ra__vtable_t.register_editor_shim_callbacks(). */ +svn_error_t * +svn_ra_serf__register_editor_shim_callbacks(svn_ra_session_t *session, + svn_delta_shim_callbacks_t *callbacks); + +/*** Authentication handler declarations ***/ + +/** + * Callback function that loads the credentials for Basic and Digest + * authentications, both for server and proxy authentication. + */ +apr_status_t +svn_ra_serf__credentials_callback(char **username, char **password, + serf_request_t *request, void *baton, + int code, const char *authn_type, + const char *realm, + apr_pool_t *pool); + + +/*** General utility functions ***/ + +/** + * Convert an HTTP STATUS_CODE resulting from a WebDAV request against + * PATH to the relevant error code. Use the response-supplied LOCATION + * where it necessary. + */ +svn_error_t * +svn_ra_serf__error_on_status(int status_code, + const char *path, + const char *location); + +/* ###? */ +svn_error_t * +svn_ra_serf__copy_into_spillbuf(svn_spillbuf_t **spillbuf, + serf_bucket_t *bkt, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/* ###? */ +serf_bucket_t * +svn_ra_serf__create_sb_bucket(svn_spillbuf_t *spillbuf, + serf_bucket_alloc_t *allocator, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool); + +/** Wrap STATUS from an serf function. If STATUS is not serf error code, + * this is equivalent to svn_error_wrap_apr(). + */ +svn_error_t * +svn_ra_serf__wrap_err(apr_status_t status, + const char *fmt, + ...); + + +#if defined(SVN_DEBUG) +/* Wrapper macros to collect file and line information */ +#define svn_ra_serf__wrap_err \ + (svn_error__locate(__FILE__,__LINE__), (svn_ra_serf__wrap_err)) + +#endif + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SVN_LIBSVN_RA_SERF_RA_SERF_H */ diff --git a/subversion/libsvn_ra_serf/replay.c b/subversion/libsvn_ra_serf/replay.c new file mode 100644 index 0000000..1fcecf4 --- /dev/null +++ b/subversion/libsvn_ra_serf/replay.c @@ -0,0 +1,920 @@ +/* + * replay.c : entry point for replay RA functions for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "../libsvn_ra/ra_loader.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_base64.h" +#include "svn_path.h" +#include "svn_private_config.h" + +#include "private/svn_string_private.h" + +#include "ra_serf.h" + + +/* + * This enum represents the current state of our XML parsing. + */ +typedef enum replay_state_e { + NONE = 0, + REPORT, + OPEN_DIR, + ADD_DIR, + OPEN_FILE, + ADD_FILE, + DELETE_ENTRY, + APPLY_TEXTDELTA, + CHANGE_PROP +} replay_state_e; + +typedef struct replay_info_t replay_info_t; + +struct replay_info_t { + apr_pool_t *pool; + + void *baton; + svn_stream_t *stream; + + replay_info_t *parent; +}; + +typedef svn_error_t * +(*change_prop_t)(void *baton, + const char *name, + const svn_string_t *value, + apr_pool_t *pool); + +typedef struct prop_info_t { + apr_pool_t *pool; + + change_prop_t change; + + const char *name; + svn_boolean_t del_prop; + + svn_stringbuf_t *prop_value; + + replay_info_t *parent; +} prop_info_t; + +typedef struct replay_context_t { + apr_pool_t *src_rev_pool; + apr_pool_t *dst_rev_pool; + /*file_pool is cleared after completion of each file. */ + apr_pool_t *file_pool; + + /* Are we done fetching this file? */ + svn_boolean_t done; + svn_ra_serf__list_t **done_list; + svn_ra_serf__list_t done_item; + + /* callback to get an editor */ + svn_ra_replay_revstart_callback_t revstart_func; + svn_ra_replay_revfinish_callback_t revfinish_func; + void *replay_baton; + + /* replay receiver function and baton */ + const svn_delta_editor_t *editor; + void *editor_baton; + + /* Path and revision used to filter replayed changes. If + INCLUDE_PATH is non-NULL, REVISION is unnecessary and will not be + included in the replay REPORT. (Because the REPORT is being + aimed an HTTP v2 revision resource.) */ + const char *include_path; + svn_revnum_t revision; + + /* Information needed to create the replay report body */ + svn_revnum_t low_water_mark; + svn_boolean_t send_deltas; + + /* Target and revision to fetch revision properties on */ + const char *revprop_target; + svn_revnum_t revprop_rev; + + /* Revision properties for this revision. */ + apr_hash_t *revs_props; + apr_hash_t *props; + + /* Keep a reference to the XML parser ctx to report any errors. */ + svn_ra_serf__xml_parser_t *parser_ctx; + + /* Handlers for the PROPFIND and REPORT for the current revision. */ + svn_ra_serf__handler_t *propfind_handler; + svn_ra_serf__handler_t *report_handler; + +} replay_context_t; + + +static void * +push_state(svn_ra_serf__xml_parser_t *parser, + replay_context_t *replay_ctx, + replay_state_e state) +{ + svn_ra_serf__xml_push_state(parser, state); + + if (state == OPEN_DIR || state == ADD_DIR || + state == OPEN_FILE || state == ADD_FILE) + { + replay_info_t *info; + + info = apr_palloc(replay_ctx->dst_rev_pool, sizeof(*info)); + + info->pool = replay_ctx->dst_rev_pool; + info->parent = parser->state->private; + info->baton = NULL; + info->stream = NULL; + + parser->state->private = info; + } + else if (state == CHANGE_PROP) + { + prop_info_t *info; + + info = apr_pcalloc(replay_ctx->dst_rev_pool, sizeof(*info)); + + info->pool = replay_ctx->dst_rev_pool; + info->parent = parser->state->private; + info->prop_value = svn_stringbuf_create_empty(info->pool); + + parser->state->private = info; + } + + return parser->state->private; +} + +static svn_error_t * +start_replay(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool) +{ + replay_context_t *ctx = parser->user_data; + replay_state_e state; + + state = parser->state->current_state; + + if (state == NONE && + strcmp(name.name, "editor-report") == 0) + { + push_state(parser, ctx, REPORT); + + /* Before we can continue, we need the revision properties. */ + SVN_ERR_ASSERT(!ctx->propfind_handler || ctx->propfind_handler->done); + + /* Create a pool for the commit editor. */ + ctx->dst_rev_pool = svn_pool_create(ctx->src_rev_pool); + ctx->file_pool = svn_pool_create(ctx->dst_rev_pool); + + SVN_ERR(svn_ra_serf__select_revprops(&ctx->props, + ctx->revprop_target, + ctx->revprop_rev, + ctx->revs_props, + ctx->dst_rev_pool, + scratch_pool)); + + if (ctx->revstart_func) + { + SVN_ERR(ctx->revstart_func(ctx->revision, ctx->replay_baton, + &ctx->editor, &ctx->editor_baton, + ctx->props, + ctx->dst_rev_pool)); + } + } + else if (state == REPORT && + strcmp(name.name, "target-revision") == 0) + { + const char *rev; + + rev = svn_xml_get_attr_value("rev", attrs); + if (!rev) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in target-revision element")); + } + + SVN_ERR(ctx->editor->set_target_revision(ctx->editor_baton, + SVN_STR_TO_REV(rev), + scratch_pool)); + } + else if (state == REPORT && + strcmp(name.name, "open-root") == 0) + { + const char *rev; + replay_info_t *info; + + rev = svn_xml_get_attr_value("rev", attrs); + + if (!rev) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-root element")); + } + + info = push_state(parser, ctx, OPEN_DIR); + + SVN_ERR(ctx->editor->open_root(ctx->editor_baton, + SVN_STR_TO_REV(rev), + ctx->dst_rev_pool, + &info->baton)); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "delete-entry") == 0) + { + const char *file_name, *rev; + replay_info_t *info; + + file_name = svn_xml_get_attr_value("name", attrs); + if (!file_name) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in delete-entry element")); + } + rev = svn_xml_get_attr_value("rev", attrs); + if (!rev) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in delete-entry element")); + } + + info = push_state(parser, ctx, DELETE_ENTRY); + + SVN_ERR(ctx->editor->delete_entry(file_name, SVN_STR_TO_REV(rev), + info->baton, scratch_pool)); + + svn_ra_serf__xml_pop_state(parser); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "open-directory") == 0) + { + const char *rev, *dir_name; + replay_info_t *info; + + dir_name = svn_xml_get_attr_value("name", attrs); + if (!dir_name) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in open-directory element")); + } + rev = svn_xml_get_attr_value("rev", attrs); + if (!rev) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-directory element")); + } + + info = push_state(parser, ctx, OPEN_DIR); + + SVN_ERR(ctx->editor->open_directory(dir_name, info->parent->baton, + SVN_STR_TO_REV(rev), + ctx->dst_rev_pool, &info->baton)); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "add-directory") == 0) + { + const char *dir_name, *copyfrom, *copyrev; + svn_revnum_t rev; + replay_info_t *info; + + dir_name = svn_xml_get_attr_value("name", attrs); + if (!dir_name) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in add-directory element")); + } + copyfrom = svn_xml_get_attr_value("copyfrom-path", attrs); + copyrev = svn_xml_get_attr_value("copyfrom-rev", attrs); + + if (copyrev) + rev = SVN_STR_TO_REV(copyrev); + else + rev = SVN_INVALID_REVNUM; + + info = push_state(parser, ctx, ADD_DIR); + + SVN_ERR(ctx->editor->add_directory(dir_name, info->parent->baton, + copyfrom, rev, + ctx->dst_rev_pool, &info->baton)); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "close-directory") == 0) + { + replay_info_t *info = parser->state->private; + + SVN_ERR(ctx->editor->close_directory(info->baton, scratch_pool)); + + svn_ra_serf__xml_pop_state(parser); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "open-file") == 0) + { + const char *file_name, *rev; + replay_info_t *info; + + svn_pool_clear(ctx->file_pool); + file_name = svn_xml_get_attr_value("name", attrs); + if (!file_name) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in open-file element")); + } + rev = svn_xml_get_attr_value("rev", attrs); + if (!rev) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-file element")); + } + + info = push_state(parser, ctx, OPEN_FILE); + + SVN_ERR(ctx->editor->open_file(file_name, info->parent->baton, + SVN_STR_TO_REV(rev), + ctx->file_pool, &info->baton)); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "add-file") == 0) + { + const char *file_name, *copyfrom, *copyrev; + svn_revnum_t rev; + replay_info_t *info; + + svn_pool_clear(ctx->file_pool); + file_name = svn_xml_get_attr_value("name", attrs); + if (!file_name) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in add-file element")); + } + copyfrom = svn_xml_get_attr_value("copyfrom-path", attrs); + copyrev = svn_xml_get_attr_value("copyfrom-rev", attrs); + + info = push_state(parser, ctx, ADD_FILE); + + if (copyrev) + rev = SVN_STR_TO_REV(copyrev); + else + rev = SVN_INVALID_REVNUM; + + SVN_ERR(ctx->editor->add_file(file_name, info->parent->baton, + copyfrom, rev, + ctx->file_pool, &info->baton)); + } + else if ((state == OPEN_FILE || state == ADD_FILE) && + strcmp(name.name, "apply-textdelta") == 0) + { + const char *checksum; + replay_info_t *info; + svn_txdelta_window_handler_t textdelta; + void *textdelta_baton; + svn_stream_t *delta_stream; + + info = push_state(parser, ctx, APPLY_TEXTDELTA); + + checksum = svn_xml_get_attr_value("checksum", attrs); + if (checksum) + { + checksum = apr_pstrdup(info->pool, checksum); + } + + SVN_ERR(ctx->editor->apply_textdelta(info->baton, checksum, + ctx->file_pool, + &textdelta, + &textdelta_baton)); + + delta_stream = svn_txdelta_parse_svndiff(textdelta, textdelta_baton, + TRUE, info->pool); + info->stream = svn_base64_decode(delta_stream, info->pool); + } + else if ((state == OPEN_FILE || state == ADD_FILE) && + strcmp(name.name, "close-file") == 0) + { + replay_info_t *info = parser->state->private; + const char *checksum; + + checksum = svn_xml_get_attr_value("checksum", attrs); + + SVN_ERR(ctx->editor->close_file(info->baton, checksum, scratch_pool)); + + svn_ra_serf__xml_pop_state(parser); + } + else if (((state == OPEN_FILE || state == ADD_FILE) && + strcmp(name.name, "change-file-prop") == 0) || + ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "change-dir-prop") == 0)) + { + const char *prop_name; + prop_info_t *info; + + prop_name = svn_xml_get_attr_value("name", attrs); + if (!prop_name) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in %s element"), + name.name); + } + + info = push_state(parser, ctx, CHANGE_PROP); + + + if (svn_xml_get_attr_value("del", attrs)) + info->del_prop = TRUE; + else + info->del_prop = FALSE; + + if (state == OPEN_FILE || state == ADD_FILE) + { + info->name = apr_pstrdup(ctx->file_pool, prop_name); + info->change = ctx->editor->change_file_prop; + } + else + { + info->name = apr_pstrdup(ctx->dst_rev_pool, prop_name); + info->change = ctx->editor->change_dir_prop; + } + + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +end_replay(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool) +{ + replay_context_t *ctx = parser->user_data; + replay_state_e state; + + state = parser->state->current_state; + + if (state == REPORT && + strcmp(name.name, "editor-report") == 0) + { + svn_ra_serf__xml_pop_state(parser); + if (ctx->revfinish_func) + { + SVN_ERR(ctx->revfinish_func(ctx->revision, ctx->replay_baton, + ctx->editor, ctx->editor_baton, + ctx->props, + ctx->dst_rev_pool)); + } + svn_pool_destroy(ctx->dst_rev_pool); + } + else if (state == OPEN_DIR && strcmp(name.name, "open-directory") == 0) + { + /* Don't do anything. */ + } + else if (state == ADD_DIR && strcmp(name.name, "add-directory") == 0) + { + /* Don't do anything. */ + } + else if (state == OPEN_FILE && strcmp(name.name, "open-file") == 0) + { + /* Don't do anything. */ + } + else if (state == ADD_FILE && strcmp(name.name, "add-file") == 0) + { + /* Don't do anything. */ + } + else if ((state == OPEN_FILE || state == ADD_FILE) && + strcmp(name.name, "close-file") == 0) + { + /* Don't do anything. */ + } + else if ((state == APPLY_TEXTDELTA) && + strcmp(name.name, "apply-textdelta") == 0) + { + replay_info_t *info = parser->state->private; + SVN_ERR(svn_stream_close(info->stream)); + svn_ra_serf__xml_pop_state(parser); + } + else if (state == CHANGE_PROP && + (strcmp(name.name, "change-file-prop") == 0 || + strcmp(name.name, "change-dir-prop") == 0)) + { + prop_info_t *info = parser->state->private; + const svn_string_t *prop_val; + + if (info->del_prop) + { + prop_val = NULL; + } + else + { + const svn_string_t *morph; + + morph = svn_stringbuf__morph_into_string(info->prop_value); +#ifdef SVN_DEBUG + info->prop_value = NULL; /* morph killed the stringbuf. */ +#endif + + if (strcmp(name.name, "change-file-prop") == 0) + prop_val = svn_base64_decode_string(morph, ctx->file_pool); + else + prop_val = svn_base64_decode_string(morph, ctx->dst_rev_pool); + } + + SVN_ERR(info->change(info->parent->baton, info->name, prop_val, + info->parent->pool)); + svn_ra_serf__xml_pop_state(parser); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +cdata_replay(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + replay_context_t *replay_ctx = parser->user_data; + replay_state_e state; + + UNUSED_CTX(replay_ctx); + + state = parser->state->current_state; + + if (state == APPLY_TEXTDELTA) + { + replay_info_t *info = parser->state->private; + apr_size_t written; + + written = len; + + SVN_ERR(svn_stream_write(info->stream, data, &written)); + + if (written != len) + return svn_error_create(SVN_ERR_STREAM_UNEXPECTED_EOF, NULL, + _("Error writing stream: unexpected EOF")); + } + else if (state == CHANGE_PROP) + { + prop_info_t *info = parser->state->private; + + svn_stringbuf_appendbytes(info->prop_value, data, len); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +create_replay_body(serf_bucket_t **bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + replay_context_t *ctx = baton; + serf_bucket_t *body_bkt; + + body_bkt = serf_bucket_aggregate_create(alloc); + + svn_ra_serf__add_open_tag_buckets(body_bkt, alloc, + "S:replay-report", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + + /* If we have a non-NULL include path, we add it to the body and + omit the revision; otherwise, the reverse. */ + if (ctx->include_path) + { + svn_ra_serf__add_tag_buckets(body_bkt, + "S:include-path", + ctx->include_path, + alloc); + } + else + { + svn_ra_serf__add_tag_buckets(body_bkt, + "S:revision", + apr_ltoa(ctx->src_rev_pool, ctx->revision), + alloc); + } + svn_ra_serf__add_tag_buckets(body_bkt, + "S:low-water-mark", + apr_ltoa(ctx->src_rev_pool, ctx->low_water_mark), + alloc); + + svn_ra_serf__add_tag_buckets(body_bkt, + "S:send-deltas", + apr_ltoa(ctx->src_rev_pool, ctx->send_deltas), + alloc); + + svn_ra_serf__add_close_tag_buckets(body_bkt, alloc, "S:replay-report"); + + *bkt = body_bkt; + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__replay(svn_ra_session_t *ra_session, + svn_revnum_t revision, + svn_revnum_t low_water_mark, + svn_boolean_t send_deltas, + const svn_delta_editor_t *editor, + void *edit_baton, + apr_pool_t *pool) +{ + replay_context_t *replay_ctx; + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_parser_t *parser_ctx; + svn_error_t *err; + const char *report_target; + + SVN_ERR(svn_ra_serf__report_resource(&report_target, session, NULL, pool)); + + replay_ctx = apr_pcalloc(pool, sizeof(*replay_ctx)); + replay_ctx->src_rev_pool = pool; + replay_ctx->editor = editor; + replay_ctx->editor_baton = edit_baton; + replay_ctx->done = FALSE; + replay_ctx->revision = revision; + replay_ctx->low_water_mark = low_water_mark; + replay_ctx->send_deltas = send_deltas; + replay_ctx->revs_props = apr_hash_make(replay_ctx->src_rev_pool); + + handler = apr_pcalloc(pool, sizeof(*handler)); + + handler->handler_pool = pool; + handler->method = "REPORT"; + handler->path = session->session_url.path; + handler->body_delegate = create_replay_body; + handler->body_delegate_baton = replay_ctx; + handler->body_type = "text/xml"; + handler->conn = session->conns[0]; + handler->session = session; + + parser_ctx = apr_pcalloc(pool, sizeof(*parser_ctx)); + + parser_ctx->pool = pool; + parser_ctx->user_data = replay_ctx; + parser_ctx->start = start_replay; + parser_ctx->end = end_replay; + parser_ctx->cdata = cdata_replay; + parser_ctx->done = &replay_ctx->done; + + handler->response_handler = svn_ra_serf__handle_xml_parser; + handler->response_baton = parser_ctx; + + /* This is only needed to handle errors during XML parsing. */ + replay_ctx->parser_ctx = parser_ctx; + replay_ctx->report_handler = handler; /* unused */ + + svn_ra_serf__request_create(handler); + + err = svn_ra_serf__context_run_wait(&replay_ctx->done, session, pool); + + SVN_ERR(svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location), + err)); + + return SVN_NO_ERROR; +} + +/* The maximum number of outstanding requests at any time. When this + * number is reached, ra_serf will stop sending requests until + * responses on the previous requests are received and handled. + * + * Some observations about serf which lead us to the current value. + * ---------------------------------------------------------------- + * + * We aim to keep serf's outgoing queue filled with enough requests so + * the network bandwidth and server capacity is used + * optimally. Originally we used 5 as the max. number of outstanding + * requests, but this turned out to be too low. + * + * Serf doesn't exit out of the svn_ra_serf__context_run_wait loop as long as + * it has data to send or receive. With small responses (revs of a few + * kB), serf doesn't come out of this loop at all. So with + * MAX_OUTSTANDING_REQUESTS set to a low number, there's a big chance + * that serf handles those requests completely in its internal loop, + * and only then gives us a chance to create new requests. This + * results in hiccups, slowing down the whole process. + * + * With a larger MAX_OUTSTANDING_REQUESTS, like 100 or more, there's + * more chance that serf can come out of its internal loop so we can + * replenish the outgoing request queue. There's no real disadvantage + * of using a large number here, besides the memory used to store the + * message, parser and handler objects (approx. 250 bytes). + * + * In my test setup peak performance was reached at max. 30-35 + * requests. So I added a small margin and chose 50. + */ +#define MAX_OUTSTANDING_REQUESTS 50 + +svn_error_t * +svn_ra_serf__replay_range(svn_ra_session_t *ra_session, + svn_revnum_t start_revision, + svn_revnum_t end_revision, + svn_revnum_t low_water_mark, + svn_boolean_t send_deltas, + svn_ra_replay_revstart_callback_t revstart_func, + svn_ra_replay_revfinish_callback_t revfinish_func, + void *replay_baton, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + svn_revnum_t rev = start_revision; + const char *report_target; + int active_reports = 0; + const char *include_path; + + SVN_ERR(svn_ra_serf__report_resource(&report_target, session, NULL, pool)); + + /* Prior to 1.8, mod_dav_svn expect to get replay REPORT requests + aimed at the session URL. But that's incorrect -- these reports + aren't about specific resources -- they are above revisions. The + path-based filtering offered by this API is just that: a filter + applied to the full set of changes made in the revision. As + such, the correct target for these REPORT requests is the "me + resource" (or, pre-http-v2, the default VCC). + + Our server should have told us if it supported this protocol + correction. If so, we aimed our report at the correct resource + and include the filtering path as metadata within the report + body. Otherwise, we fall back to the pre-1.8 behavior and just + wish for the best. + + See issue #4287: + http://subversion.tigris.org/issues/show_bug.cgi?id=4287 + */ + if (session->supports_rev_rsrc_replay) + { + SVN_ERR(svn_ra_serf__get_relative_path(&include_path, + session->session_url.path, + session, session->conns[0], + pool)); + } + else + { + include_path = NULL; + } + + while (active_reports || rev <= end_revision) + { + svn_ra_serf__list_t *done_list; + svn_ra_serf__list_t *done_reports = NULL; + replay_context_t *replay_ctx; + + if (session->cancel_func) + SVN_ERR(session->cancel_func(session->cancel_baton)); + + /* Send pending requests, if any. Limit the number of outstanding + requests to MAX_OUTSTANDING_REQUESTS. */ + if (rev <= end_revision && active_reports < MAX_OUTSTANDING_REQUESTS) + { + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_parser_t *parser_ctx; + apr_pool_t *ctx_pool = svn_pool_create(pool); + const char *replay_target; + + replay_ctx = apr_pcalloc(ctx_pool, sizeof(*replay_ctx)); + replay_ctx->src_rev_pool = ctx_pool; + replay_ctx->revstart_func = revstart_func; + replay_ctx->revfinish_func = revfinish_func; + replay_ctx->replay_baton = replay_baton; + replay_ctx->done = FALSE; + replay_ctx->include_path = include_path; + replay_ctx->revision = rev; + replay_ctx->low_water_mark = low_water_mark; + replay_ctx->send_deltas = send_deltas; + replay_ctx->done_item.data = replay_ctx; + + /* Request all properties of a certain revision. */ + replay_ctx->revs_props = apr_hash_make(replay_ctx->src_rev_pool); + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + { + replay_ctx->revprop_target = apr_psprintf(pool, "%s/%ld", + session->rev_stub, rev); + replay_ctx->revprop_rev = SVN_INVALID_REVNUM; + } + else + { + replay_ctx->revprop_target = report_target; + replay_ctx->revprop_rev = rev; + } + + SVN_ERR(svn_ra_serf__deliver_props(&replay_ctx->propfind_handler, + replay_ctx->revs_props, session, + session->conns[0], + replay_ctx->revprop_target, + replay_ctx->revprop_rev, + "0", all_props, + NULL, + replay_ctx->src_rev_pool)); + + /* Spin up the serf request for the PROPFIND. */ + svn_ra_serf__request_create(replay_ctx->propfind_handler); + + /* Send the replay REPORT request. */ + if (session->supports_rev_rsrc_replay) + { + replay_target = apr_psprintf(pool, "%s/%ld", + session->rev_stub, rev); + } + else + { + replay_target = session->session_url.path; + } + + handler = apr_pcalloc(replay_ctx->src_rev_pool, sizeof(*handler)); + + handler->handler_pool = replay_ctx->src_rev_pool; + handler->method = "REPORT"; + handler->path = replay_target; + handler->body_delegate = create_replay_body; + handler->body_delegate_baton = replay_ctx; + handler->conn = session->conns[0]; + handler->session = session; + + parser_ctx = apr_pcalloc(replay_ctx->src_rev_pool, + sizeof(*parser_ctx)); + + /* Setup the XML parser context. + Because we have not one but a list of requests, the 'done' property + on the replay_ctx is not of much use. Instead, use 'done_list'. + On each handled response (succesfully or not), the parser will add + done_item to done_list, so by keeping track of the state of + done_list we know how many requests have been handled completely. + */ + parser_ctx->pool = replay_ctx->src_rev_pool; + parser_ctx->user_data = replay_ctx; + parser_ctx->start = start_replay; + parser_ctx->end = end_replay; + parser_ctx->cdata = cdata_replay; + parser_ctx->done = &replay_ctx->done; + parser_ctx->done_list = &done_reports; + parser_ctx->done_item = &replay_ctx->done_item; + handler->response_handler = svn_ra_serf__handle_xml_parser; + handler->response_baton = parser_ctx; + replay_ctx->report_handler = handler; + + /* This is only needed to handle errors during XML parsing. */ + replay_ctx->parser_ctx = parser_ctx; + + svn_ra_serf__request_create(handler); + + rev++; + active_reports++; + } + + /* Run the serf loop. */ + SVN_ERR(svn_ra_serf__context_run_wait(&replay_ctx->done, session, pool)); + + /* Substract the number of completely handled responses from our + total nr. of open requests', so we'll know when to stop this loop. + Since the message is completely handled, we can destroy its pool. */ + done_list = done_reports; + while (done_list) + { + replay_context_t *ctx = (replay_context_t *)done_list->data; + svn_ra_serf__handler_t *done_handler = ctx->report_handler; + + done_list = done_list->next; + SVN_ERR(svn_ra_serf__error_on_status(done_handler->sline.code, + done_handler->path, + done_handler->location)); + svn_pool_destroy(ctx->src_rev_pool); + active_reports--; + } + + done_reports = NULL; + } + + return SVN_NO_ERROR; +} +#undef MAX_OUTSTANDING_REQUESTS diff --git a/subversion/libsvn_ra_serf/sb_bucket.c b/subversion/libsvn_ra_serf/sb_bucket.c new file mode 100644 index 0000000..df0541f --- /dev/null +++ b/subversion/libsvn_ra_serf/sb_bucket.c @@ -0,0 +1,185 @@ +/* + * sb_bucket.c : a serf bucket that wraps a spillbuf + * + * ==================================================================== + * 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 <serf.h> +#include <serf_bucket_util.h> + +#include "svn_private_config.h" +#include "private/svn_subr_private.h" + +#include "ra_serf.h" + +#define SB_BLOCKSIZE 1024 +#define SB_MAXSIZE 32768 + + +struct sbb_baton +{ + svn_spillbuf_t *spillbuf; + + const char *holding; + apr_size_t hold_len; + + apr_pool_t *scratch_pool; +}; + + +svn_error_t * +svn_ra_serf__copy_into_spillbuf(svn_spillbuf_t **spillbuf, + serf_bucket_t *bkt, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + *spillbuf = svn_spillbuf__create(SB_BLOCKSIZE, SB_MAXSIZE, result_pool); + + /* Copy all data from the bucket into the spillbuf. */ + while (TRUE) + { + apr_status_t status; + const char *data; + apr_size_t len; + + status = serf_bucket_read(bkt, SERF_READ_ALL_AVAIL, &data, &len); + + if (status != APR_SUCCESS && status != APR_EOF) + return svn_ra_serf__wrap_err(status, _("Failed to read the request")); + + SVN_ERR(svn_spillbuf__write(*spillbuf, data, len, scratch_pool)); + + if (status == APR_EOF) + break; + } + + return SVN_NO_ERROR; +} + + +static apr_status_t +sb_bucket_read(serf_bucket_t *bucket, apr_size_t requested, + const char **data, apr_size_t *len) +{ + struct sbb_baton *sbb = bucket->data; + svn_error_t *err; + + if (sbb->holding) + { + *data = sbb->holding; + + if (requested < sbb->hold_len) + { + *len = requested; + sbb->holding += requested; + sbb->hold_len -= requested; + return APR_SUCCESS; + } + + /* Return whatever we're holding, and then forget (consume) it. */ + *len = sbb->hold_len; + sbb->holding = NULL; + return APR_SUCCESS; + } + + err = svn_spillbuf__read(data, len, sbb->spillbuf, sbb->scratch_pool); + svn_pool_clear(sbb->scratch_pool); + + /* ### do something with this */ + svn_error_clear(err); + + /* The spillbuf may have returned more than requested. Stash any extra + into our holding area. */ + if (requested < *len) + { + sbb->holding = *data + requested; + sbb->hold_len = *len - requested; + *len = requested; + } + + return *data == NULL ? APR_EOF : APR_SUCCESS; +} + + +static apr_status_t +sb_bucket_readline(serf_bucket_t *bucket, int acceptable, + int *found, + const char **data, apr_size_t *len) +{ + /* ### for now, we know callers won't use this function. */ + svn_error_clear(svn_error__malfunction(TRUE, __FILE__, __LINE__, + "Not implemented.")); + return APR_ENOTIMPL; +} + + +static apr_status_t +sb_bucket_peek(serf_bucket_t *bucket, + const char **data, apr_size_t *len) +{ + struct sbb_baton *sbb = bucket->data; + svn_error_t *err; + + /* If we're not holding any data, then fill it. */ + if (sbb->holding == NULL) + { + err = svn_spillbuf__read(&sbb->holding, &sbb->hold_len, sbb->spillbuf, + sbb->scratch_pool); + svn_pool_clear(sbb->scratch_pool); + + /* ### do something with this */ + svn_error_clear(err); + } + + /* Return the data we are (now) holding. */ + *data = sbb->holding; + *len = sbb->hold_len; + + return *data == NULL ? APR_EOF : APR_SUCCESS; +} + + +static const serf_bucket_type_t sb_bucket_vtable = { + "SPILLBUF", + sb_bucket_read, + sb_bucket_readline, + serf_default_read_iovec, + serf_default_read_for_sendfile, + serf_default_read_bucket, + sb_bucket_peek, + serf_default_destroy_and_data, +}; + + +serf_bucket_t * +svn_ra_serf__create_sb_bucket(svn_spillbuf_t *spillbuf, + serf_bucket_alloc_t *allocator, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + struct sbb_baton *sbb; + + sbb = serf_bucket_mem_alloc(allocator, sizeof(*sbb)); + sbb->spillbuf = spillbuf; + sbb->holding = NULL; + sbb->scratch_pool = svn_pool_create(result_pool); + + return serf_bucket_create(&sb_bucket_vtable, allocator, sbb); +} diff --git a/subversion/libsvn_ra_serf/serf.c b/subversion/libsvn_ra_serf/serf.c new file mode 100644 index 0000000..6016157 --- /dev/null +++ b/subversion/libsvn_ra_serf/serf.c @@ -0,0 +1,1246 @@ +/* + * serf.c : entry point for ra_serf + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + + +#define APR_WANT_STRFUNC +#include <apr_want.h> + +#include <apr_uri.h> +#include <serf.h> + +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "../libsvn_ra/ra_loader.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_dirent_uri.h" +#include "svn_hash.h" +#include "svn_path.h" +#include "svn_time.h" +#include "svn_version.h" + +#include "private/svn_dav_protocol.h" +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_subr_private.h" +#include "svn_private_config.h" + +#include "ra_serf.h" + + +/* Implements svn_ra__vtable_t.get_version(). */ +static const svn_version_t * +ra_serf_version(void) +{ + SVN_VERSION_BODY; +} + +#define RA_SERF_DESCRIPTION \ + N_("Module for accessing a repository via WebDAV protocol using serf.") + +/* Implements svn_ra__vtable_t.get_description(). */ +static const char * +ra_serf_get_description(void) +{ + return _(RA_SERF_DESCRIPTION); +} + +/* Implements svn_ra__vtable_t.get_schemes(). */ +static const char * const * +ra_serf_get_schemes(apr_pool_t *pool) +{ + static const char *serf_ssl[] = { "http", "https", NULL }; +#if 0 + /* ### Temporary: to shut up a warning. */ + static const char *serf_no_ssl[] = { "http", NULL }; +#endif + + /* TODO: Runtime detection. */ + return serf_ssl; +} + +/* Load the setting http-auth-types from the global or server specific + section, parse its value and set the types of authentication we should + accept from the server. */ +static svn_error_t * +load_http_auth_types(apr_pool_t *pool, svn_config_t *config, + const char *server_group, + int *authn_types) +{ + const char *http_auth_types = NULL; + *authn_types = SERF_AUTHN_NONE; + + svn_config_get(config, &http_auth_types, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, NULL); + + if (server_group) + { + svn_config_get(config, &http_auth_types, server_group, + SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, http_auth_types); + } + + if (http_auth_types) + { + char *token; + char *auth_types_list = apr_palloc(pool, strlen(http_auth_types) + 1); + apr_collapse_spaces(auth_types_list, http_auth_types); + while ((token = svn_cstring_tokenize(";", &auth_types_list)) != NULL) + { + if (svn_cstring_casecmp("basic", token) == 0) + *authn_types |= SERF_AUTHN_BASIC; + else if (svn_cstring_casecmp("digest", token) == 0) + *authn_types |= SERF_AUTHN_DIGEST; + else if (svn_cstring_casecmp("ntlm", token) == 0) + *authn_types |= SERF_AUTHN_NTLM; + else if (svn_cstring_casecmp("negotiate", token) == 0) + *authn_types |= SERF_AUTHN_NEGOTIATE; + else + return svn_error_createf(SVN_ERR_BAD_CONFIG_VALUE, NULL, + _("Invalid config: unknown %s " + "'%s'"), + SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, token); + } + } + else + { + /* Nothing specified by the user, so accept all types. */ + *authn_types = SERF_AUTHN_ALL; + } + + return SVN_NO_ERROR; +} + +/* Default HTTP timeout (in seconds); overridden by the 'http-timeout' + runtime configuration variable. */ +#define DEFAULT_HTTP_TIMEOUT 600 + +static svn_error_t * +load_config(svn_ra_serf__session_t *session, + apr_hash_t *config_hash, + apr_pool_t *pool) +{ + svn_config_t *config, *config_client; + const char *server_group; + const char *proxy_host = NULL; + const char *port_str = NULL; + const char *timeout_str = NULL; + const char *exceptions; + apr_port_t proxy_port; + + if (config_hash) + { + config = svn_hash_gets(config_hash, SVN_CONFIG_CATEGORY_SERVERS); + config_client = svn_hash_gets(config_hash, SVN_CONFIG_CATEGORY_CONFIG); + } + else + { + config = NULL; + config_client = NULL; + } + + SVN_ERR(svn_config_get_bool(config, &session->using_compression, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_COMPRESSION, TRUE)); + svn_config_get(config, &timeout_str, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_TIMEOUT, NULL); + + if (session->wc_callbacks->auth_baton) + { + if (config_client) + { + svn_auth_set_parameter(session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_CONFIG_CATEGORY_CONFIG, + config_client); + } + if (config) + { + svn_auth_set_parameter(session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERS, + config); + } + } + + /* Use the default proxy-specific settings if and only if + "http-proxy-exceptions" is not set to exclude this host. */ + svn_config_get(config, &exceptions, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_PROXY_EXCEPTIONS, ""); + if (! svn_cstring_match_glob_list(session->session_url.hostname, + svn_cstring_split(exceptions, ",", + TRUE, pool))) + { + svn_config_get(config, &proxy_host, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_PROXY_HOST, NULL); + svn_config_get(config, &port_str, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_PROXY_PORT, NULL); + svn_config_get(config, &session->proxy_username, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_PROXY_USERNAME, NULL); + svn_config_get(config, &session->proxy_password, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_PROXY_PASSWORD, NULL); + } + + /* Load the global ssl settings, if set. */ + SVN_ERR(svn_config_get_bool(config, &session->trust_default_ca, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_SSL_TRUST_DEFAULT_CA, + TRUE)); + svn_config_get(config, &session->ssl_authorities, SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_SSL_AUTHORITY_FILES, NULL); + + /* If set, read the flag that tells us to do bulk updates or not. Defaults + to skelta updates. */ + SVN_ERR(svn_config_get_tristate(config, &session->bulk_updates, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, + "auto", + svn_tristate_unknown)); + + /* Load the maximum number of parallel session connections. */ + SVN_ERR(svn_config_get_int64(config, &session->max_connections, + SVN_CONFIG_SECTION_GLOBAL, + SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, + SVN_CONFIG_DEFAULT_OPTION_HTTP_MAX_CONNECTIONS)); + + if (config) + server_group = svn_config_find_group(config, + session->session_url.hostname, + SVN_CONFIG_SECTION_GROUPS, pool); + else + server_group = NULL; + + if (server_group) + { + SVN_ERR(svn_config_get_bool(config, &session->using_compression, + server_group, + SVN_CONFIG_OPTION_HTTP_COMPRESSION, + session->using_compression)); + svn_config_get(config, &timeout_str, server_group, + SVN_CONFIG_OPTION_HTTP_TIMEOUT, timeout_str); + + svn_auth_set_parameter(session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_SERVER_GROUP, server_group); + + /* Load the group proxy server settings, overriding global + settings. We intentionally ignore 'http-proxy-exceptions' + here because, well, if this site was an exception, why is + there a per-server proxy configuration for it? */ + svn_config_get(config, &proxy_host, server_group, + SVN_CONFIG_OPTION_HTTP_PROXY_HOST, proxy_host); + svn_config_get(config, &port_str, server_group, + SVN_CONFIG_OPTION_HTTP_PROXY_PORT, port_str); + svn_config_get(config, &session->proxy_username, server_group, + SVN_CONFIG_OPTION_HTTP_PROXY_USERNAME, + session->proxy_username); + svn_config_get(config, &session->proxy_password, server_group, + SVN_CONFIG_OPTION_HTTP_PROXY_PASSWORD, + session->proxy_password); + + /* Load the group ssl settings. */ + SVN_ERR(svn_config_get_bool(config, &session->trust_default_ca, + server_group, + SVN_CONFIG_OPTION_SSL_TRUST_DEFAULT_CA, + session->trust_default_ca)); + svn_config_get(config, &session->ssl_authorities, server_group, + SVN_CONFIG_OPTION_SSL_AUTHORITY_FILES, + session->ssl_authorities); + + /* Load the group bulk updates flag. */ + SVN_ERR(svn_config_get_tristate(config, &session->bulk_updates, + server_group, + SVN_CONFIG_OPTION_HTTP_BULK_UPDATES, + "auto", + session->bulk_updates)); + + /* Load the maximum number of parallel session connections, + overriding global values. */ + SVN_ERR(svn_config_get_int64(config, &session->max_connections, + server_group, + SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS, + session->max_connections)); + } + + /* Don't allow the http-max-connections value to be larger than our + compiled-in limit, or to be too small to operate. Broken + functionality and angry administrators are equally undesirable. */ + if (session->max_connections > SVN_RA_SERF__MAX_CONNECTIONS_LIMIT) + session->max_connections = SVN_RA_SERF__MAX_CONNECTIONS_LIMIT; + if (session->max_connections < 2) + session->max_connections = 2; + + /* Parse the connection timeout value, if any. */ + session->timeout = apr_time_from_sec(DEFAULT_HTTP_TIMEOUT); + if (timeout_str) + { + char *endstr; + const long int timeout = strtol(timeout_str, &endstr, 10); + + if (*endstr) + return svn_error_create(SVN_ERR_BAD_CONFIG_VALUE, NULL, + _("Invalid config: illegal character in " + "timeout value")); + if (timeout < 0) + return svn_error_create(SVN_ERR_BAD_CONFIG_VALUE, NULL, + _("Invalid config: negative timeout value")); + session->timeout = apr_time_from_sec(timeout); + } + SVN_ERR_ASSERT(session->timeout >= 0); + + /* Convert the proxy port value, if any. */ + if (port_str) + { + char *endstr; + const long int port = strtol(port_str, &endstr, 10); + + if (*endstr) + return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("Invalid URL: illegal character in proxy " + "port number")); + if (port < 0) + return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("Invalid URL: negative proxy port number")); + if (port > 65535) + return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("Invalid URL: proxy port number greater " + "than maximum TCP port number 65535")); + proxy_port = (apr_port_t) port; + } + else + { + proxy_port = 80; + } + + if (proxy_host) + { + apr_sockaddr_t *proxy_addr; + apr_status_t status; + + status = apr_sockaddr_info_get(&proxy_addr, proxy_host, + APR_UNSPEC, proxy_port, 0, + session->pool); + if (status) + { + return svn_ra_serf__wrap_err( + status, _("Could not resolve proxy server '%s'"), + proxy_host); + } + session->using_proxy = TRUE; + serf_config_proxy(session->context, proxy_addr); + } + else + { + session->using_proxy = FALSE; + } + + /* Setup authentication. */ + SVN_ERR(load_http_auth_types(pool, config, server_group, + &session->authn_types)); + serf_config_authn_types(session->context, session->authn_types); + serf_config_credentials_callback(session->context, + svn_ra_serf__credentials_callback); + + return SVN_NO_ERROR; +} +#undef DEFAULT_HTTP_TIMEOUT + +static void +svn_ra_serf__progress(void *progress_baton, apr_off_t read, apr_off_t written) +{ + const svn_ra_serf__session_t *serf_sess = progress_baton; + if (serf_sess->progress_func) + { + serf_sess->progress_func(read + written, -1, + serf_sess->progress_baton, + serf_sess->pool); + } +} + +/* Implements svn_ra__vtable_t.open_session(). */ +static svn_error_t * +svn_ra_serf__open(svn_ra_session_t *session, + const char **corrected_url, + const char *session_URL, + const svn_ra_callbacks2_t *callbacks, + void *callback_baton, + apr_hash_t *config, + apr_pool_t *pool) +{ + apr_status_t status; + svn_ra_serf__session_t *serf_sess; + apr_uri_t url; + const char *client_string = NULL; + + if (corrected_url) + *corrected_url = NULL; + + serf_sess = apr_pcalloc(pool, sizeof(*serf_sess)); + serf_sess->pool = svn_pool_create(pool); + serf_sess->wc_callbacks = callbacks; + serf_sess->wc_callback_baton = callback_baton; + serf_sess->progress_func = callbacks->progress_func; + serf_sess->progress_baton = callbacks->progress_baton; + serf_sess->cancel_func = callbacks->cancel_func; + serf_sess->cancel_baton = callback_baton; + + /* todo: reuse serf context across sessions */ + serf_sess->context = serf_context_create(serf_sess->pool); + + SVN_ERR(svn_ra_serf__blncache_create(&serf_sess->blncache, + serf_sess->pool)); + + + status = apr_uri_parse(serf_sess->pool, session_URL, &url); + if (status) + { + return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("Illegal URL '%s'"), + session_URL); + } + /* Depending the version of apr-util in use, for root paths url.path + will be NULL or "", where serf requires "/". */ + if (url.path == NULL || url.path[0] == '\0') + { + url.path = apr_pstrdup(serf_sess->pool, "/"); + } + if (!url.port) + { + url.port = apr_uri_port_of_scheme(url.scheme); + } + serf_sess->session_url = url; + serf_sess->session_url_str = apr_pstrdup(serf_sess->pool, session_URL); + serf_sess->using_ssl = (svn_cstring_casecmp(url.scheme, "https") == 0); + + serf_sess->supports_deadprop_count = svn_tristate_unknown; + + serf_sess->capabilities = apr_hash_make(serf_sess->pool); + + /* We have to assume that the server only supports HTTP/1.0. Once it's clear + HTTP/1.1 is supported, we can upgrade. */ + serf_sess->http10 = TRUE; + + SVN_ERR(load_config(serf_sess, config, serf_sess->pool)); + + serf_sess->conns[0] = apr_pcalloc(serf_sess->pool, + sizeof(*serf_sess->conns[0])); + serf_sess->conns[0]->bkt_alloc = + serf_bucket_allocator_create(serf_sess->pool, NULL, NULL); + serf_sess->conns[0]->session = serf_sess; + serf_sess->conns[0]->last_status_code = -1; + + /* create the user agent string */ + if (callbacks->get_client_string) + SVN_ERR(callbacks->get_client_string(callback_baton, &client_string, pool)); + + if (client_string) + serf_sess->useragent = apr_pstrcat(pool, USER_AGENT, " ", + client_string, (char *)NULL); + else + serf_sess->useragent = USER_AGENT; + + /* go ahead and tell serf about the connection. */ + status = + serf_connection_create2(&serf_sess->conns[0]->conn, + serf_sess->context, + url, + svn_ra_serf__conn_setup, serf_sess->conns[0], + svn_ra_serf__conn_closed, serf_sess->conns[0], + serf_sess->pool); + if (status) + return svn_ra_serf__wrap_err(status, NULL); + + /* Set the progress callback. */ + serf_context_set_progress_cb(serf_sess->context, svn_ra_serf__progress, + serf_sess); + + serf_sess->num_conns = 1; + + session->priv = serf_sess; + + return svn_ra_serf__exchange_capabilities(serf_sess, corrected_url, pool); +} + +/* Implements svn_ra__vtable_t.reparent(). */ +static svn_error_t * +svn_ra_serf__reparent(svn_ra_session_t *ra_session, + const char *url, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_uri_t new_url; + apr_status_t status; + + /* If it's the URL we already have, wave our hands and do nothing. */ + if (strcmp(session->session_url_str, url) == 0) + { + return SVN_NO_ERROR; + } + + if (!session->repos_root_str) + { + const char *vcc_url; + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool)); + } + + if (!svn_uri__is_ancestor(session->repos_root_str, url)) + { + return svn_error_createf( + SVN_ERR_RA_ILLEGAL_URL, NULL, + _("URL '%s' is not a child of the session's repository root " + "URL '%s'"), url, session->repos_root_str); + } + + status = apr_uri_parse(pool, url, &new_url); + if (status) + { + return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL, + _("Illegal repository URL '%s'"), url); + } + + /* Depending the version of apr-util in use, for root paths url.path + will be NULL or "", where serf requires "/". */ + /* ### Maybe we should use a string buffer for these strings so we + ### don't allocate memory in the session on every reparent? */ + if (new_url.path == NULL || new_url.path[0] == '\0') + { + session->session_url.path = apr_pstrdup(session->pool, "/"); + } + else + { + session->session_url.path = apr_pstrdup(session->pool, new_url.path); + } + session->session_url_str = apr_pstrdup(session->pool, url); + + return SVN_NO_ERROR; +} + +/* Implements svn_ra__vtable_t.get_session_url(). */ +static svn_error_t * +svn_ra_serf__get_session_url(svn_ra_session_t *ra_session, + const char **url, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + *url = apr_pstrdup(pool, session->session_url_str); + return SVN_NO_ERROR; +} + +/* Implements svn_ra__vtable_t.get_latest_revnum(). */ +static svn_error_t * +svn_ra_serf__get_latest_revnum(svn_ra_session_t *ra_session, + svn_revnum_t *latest_revnum, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + return svn_error_trace(svn_ra_serf__get_youngest_revnum( + latest_revnum, session, pool)); +} + +/* Implements svn_ra__vtable_t.rev_proplist(). */ +static svn_error_t * +svn_ra_serf__rev_proplist(svn_ra_session_t *ra_session, + svn_revnum_t rev, + apr_hash_t **ret_props, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_hash_t *props; + const char *propfind_path; + + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + { + propfind_path = apr_psprintf(pool, "%s/%ld", session->rev_stub, rev); + + /* svn_ra_serf__retrieve_props() wants to added the revision as + a Label to the PROPFIND, which isn't really necessary when + querying a rev-stub URI. *Shrug* Probably okay to leave the + Label, but whatever. */ + rev = SVN_INVALID_REVNUM; + } + else + { + /* Use the VCC as the propfind target path. */ + SVN_ERR(svn_ra_serf__discover_vcc(&propfind_path, session, NULL, pool)); + } + + /* ### fix: fetch hash of *just* the PATH@REV props. no nested hash. */ + SVN_ERR(svn_ra_serf__retrieve_props(&props, session, session->conns[0], + propfind_path, rev, "0", all_props, + pool, pool)); + + SVN_ERR(svn_ra_serf__select_revprops(ret_props, propfind_path, rev, props, + pool, pool)); + + return SVN_NO_ERROR; +} + +/* Implements svn_ra__vtable_t.rev_prop(). */ +static svn_error_t * +svn_ra_serf__rev_prop(svn_ra_session_t *session, + svn_revnum_t rev, + const char *name, + svn_string_t **value, + apr_pool_t *pool) +{ + apr_hash_t *props; + + SVN_ERR(svn_ra_serf__rev_proplist(session, rev, &props, pool)); + + *value = svn_hash_gets(props, name); + + return SVN_NO_ERROR; +} + +static svn_error_t * +fetch_path_props(apr_hash_t **props, + svn_ra_serf__session_t *session, + const char *session_relpath, + svn_revnum_t revision, + const svn_ra_serf__dav_props_t *desired_props, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + const char *url; + + url = session->session_url.path; + + /* If we have a relative path, append it. */ + if (session_relpath) + url = svn_path_url_add_component2(url, session_relpath, scratch_pool); + + /* If we were given a specific revision, get a URL that refers to that + specific revision (rather than floating with HEAD). */ + if (SVN_IS_VALID_REVNUM(revision)) + { + SVN_ERR(svn_ra_serf__get_stable_url(&url, NULL /* latest_revnum */, + session, NULL /* conn */, + url, revision, + scratch_pool, scratch_pool)); + } + + /* URL is stable, so we use SVN_INVALID_REVNUM since it is now irrelevant. + Or we started with SVN_INVALID_REVNUM and URL may be floating. */ + SVN_ERR(svn_ra_serf__fetch_node_props(props, session->conns[0], + url, SVN_INVALID_REVNUM, + desired_props, + result_pool, scratch_pool)); + + return SVN_NO_ERROR; +} + +/* Implements svn_ra__vtable_t.check_path(). */ +static svn_error_t * +svn_ra_serf__check_path(svn_ra_session_t *ra_session, + const char *rel_path, + svn_revnum_t revision, + svn_node_kind_t *kind, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_hash_t *props; + + svn_error_t *err = fetch_path_props(&props, session, rel_path, + revision, check_path_props, + pool, pool); + + if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *kind = svn_node_none; + } + else + { + /* Any other error, raise to caller. */ + if (err) + return svn_error_trace(err); + + SVN_ERR(svn_ra_serf__get_resource_type(kind, props)); + } + + return SVN_NO_ERROR; +} + + +struct dirent_walker_baton_t { + /* Update the fields in this entry. */ + svn_dirent_t *entry; + + svn_tristate_t *supports_deadprop_count; + + /* If allocations are necessary, then use this pool. */ + apr_pool_t *result_pool; +}; + +static svn_error_t * +dirent_walker(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + struct dirent_walker_baton_t *dwb = baton; + + if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0) + { + dwb->entry->has_props = TRUE; + } + else if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0) + { + dwb->entry->has_props = TRUE; + } + else if (strcmp(ns, SVN_DAV_PROP_NS_DAV) == 0) + { + if(strcmp(name, "deadprop-count") == 0) + { + if (*val->data) + { + apr_int64_t deadprop_count; + SVN_ERR(svn_cstring_atoi64(&deadprop_count, val->data)); + dwb->entry->has_props = deadprop_count > 0; + if (dwb->supports_deadprop_count) + *dwb->supports_deadprop_count = svn_tristate_true; + } + else if (dwb->supports_deadprop_count) + *dwb->supports_deadprop_count = svn_tristate_false; + } + } + else if (strcmp(ns, "DAV:") == 0) + { + if (strcmp(name, SVN_DAV__VERSION_NAME) == 0) + { + dwb->entry->created_rev = SVN_STR_TO_REV(val->data); + } + else if (strcmp(name, "creator-displayname") == 0) + { + dwb->entry->last_author = val->data; + } + else if (strcmp(name, SVN_DAV__CREATIONDATE) == 0) + { + SVN_ERR(svn_time_from_cstring(&dwb->entry->time, + val->data, + dwb->result_pool)); + } + else if (strcmp(name, "getcontentlength") == 0) + { + /* 'getcontentlength' property is empty for directories. */ + if (val->len) + { + SVN_ERR(svn_cstring_atoi64(&dwb->entry->size, val->data)); + } + } + else if (strcmp(name, "resourcetype") == 0) + { + if (strcmp(val->data, "collection") == 0) + { + dwb->entry->kind = svn_node_dir; + } + else + { + dwb->entry->kind = svn_node_file; + } + } + } + + return SVN_NO_ERROR; +} + +struct path_dirent_visitor_t { + apr_hash_t *full_paths; + apr_hash_t *base_paths; + const char *orig_path; + svn_tristate_t supports_deadprop_count; + apr_pool_t *result_pool; +}; + +static svn_error_t * +path_dirent_walker(void *baton, + const char *path, apr_ssize_t path_len, + const char *ns, apr_ssize_t ns_len, + const char *name, apr_ssize_t name_len, + const svn_string_t *val, + apr_pool_t *pool) +{ + struct path_dirent_visitor_t *dirents = baton; + struct dirent_walker_baton_t dwb; + svn_dirent_t *entry; + + /* Skip our original path. */ + if (strcmp(path, dirents->orig_path) == 0) + { + return SVN_NO_ERROR; + } + + entry = apr_hash_get(dirents->full_paths, path, path_len); + + if (!entry) + { + const char *base_name; + + entry = svn_dirent_create(pool); + + apr_hash_set(dirents->full_paths, path, path_len, entry); + + base_name = svn_path_uri_decode(svn_urlpath__basename(path, pool), + pool); + + svn_hash_sets(dirents->base_paths, base_name, entry); + } + + dwb.entry = entry; + dwb.supports_deadprop_count = &dirents->supports_deadprop_count; + dwb.result_pool = dirents->result_pool; + return svn_error_trace(dirent_walker(&dwb, ns, name, val, pool)); +} + +static const svn_ra_serf__dav_props_t * +get_dirent_props(apr_uint32_t dirent_fields, + svn_ra_serf__session_t *session, + apr_pool_t *pool) +{ + svn_ra_serf__dav_props_t *prop; + apr_array_header_t *props = apr_array_make + (pool, 7, sizeof(svn_ra_serf__dav_props_t)); + + if (session->supports_deadprop_count != svn_tristate_false + || ! (dirent_fields & SVN_DIRENT_HAS_PROPS)) + { + if (dirent_fields & SVN_DIRENT_KIND) + { + prop = apr_array_push(props); + prop->namespace = "DAV:"; + prop->name = "resourcetype"; + } + + if (dirent_fields & SVN_DIRENT_SIZE) + { + prop = apr_array_push(props); + prop->namespace = "DAV:"; + prop->name = "getcontentlength"; + } + + if (dirent_fields & SVN_DIRENT_HAS_PROPS) + { + prop = apr_array_push(props); + prop->namespace = SVN_DAV_PROP_NS_DAV; + prop->name = "deadprop-count"; + } + + if (dirent_fields & SVN_DIRENT_CREATED_REV) + { + svn_ra_serf__dav_props_t *p = apr_array_push(props); + p->namespace = "DAV:"; + p->name = SVN_DAV__VERSION_NAME; + } + + if (dirent_fields & SVN_DIRENT_TIME) + { + prop = apr_array_push(props); + prop->namespace = "DAV:"; + prop->name = SVN_DAV__CREATIONDATE; + } + + if (dirent_fields & SVN_DIRENT_LAST_AUTHOR) + { + prop = apr_array_push(props); + prop->namespace = "DAV:"; + prop->name = "creator-displayname"; + } + } + else + { + /* We found an old subversion server that can't handle + the deadprop-count property in the way we expect. + + The neon behavior is to retrieve all properties in this case */ + prop = apr_array_push(props); + prop->namespace = "DAV:"; + prop->name = "allprop"; + } + + prop = apr_array_push(props); + prop->namespace = NULL; + prop->name = NULL; + + return (svn_ra_serf__dav_props_t *) props->elts; +} + +/* Implements svn_ra__vtable_t.stat(). */ +static svn_error_t * +svn_ra_serf__stat(svn_ra_session_t *ra_session, + const char *rel_path, + svn_revnum_t revision, + svn_dirent_t **dirent, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_hash_t *props; + svn_error_t *err; + struct dirent_walker_baton_t dwb; + svn_tristate_t deadprop_count = svn_tristate_unknown; + + err = fetch_path_props(&props, + session, rel_path, revision, + get_dirent_props(SVN_DIRENT_ALL, session, pool), + pool, pool); + if (err) + { + if (err->apr_err == SVN_ERR_FS_NOT_FOUND) + { + svn_error_clear(err); + *dirent = NULL; + return SVN_NO_ERROR; + } + else + return svn_error_trace(err); + } + + dwb.entry = svn_dirent_create(pool); + dwb.supports_deadprop_count = &deadprop_count; + dwb.result_pool = pool; + SVN_ERR(svn_ra_serf__walk_node_props(props, dirent_walker, &dwb, pool)); + + if (deadprop_count == svn_tristate_false + && session->supports_deadprop_count == svn_tristate_unknown + && !dwb.entry->has_props) + { + /* We have to requery as the server didn't give us the right + information */ + session->supports_deadprop_count = svn_tristate_false; + + SVN_ERR(fetch_path_props(&props, + session, rel_path, SVN_INVALID_REVNUM, + get_dirent_props(SVN_DIRENT_ALL, session, pool), + pool, pool)); + + SVN_ERR(svn_ra_serf__walk_node_props(props, dirent_walker, &dwb, pool)); + } + + if (deadprop_count != svn_tristate_unknown) + session->supports_deadprop_count = deadprop_count; + + *dirent = dwb.entry; + + return SVN_NO_ERROR; +} + +/* Reads the 'resourcetype' property from the list PROPS and checks if the + * resource at PATH@REVISION really is a directory. Returns + * SVN_ERR_FS_NOT_DIRECTORY if not. + */ +static svn_error_t * +resource_is_directory(apr_hash_t *props) +{ + svn_node_kind_t kind; + + SVN_ERR(svn_ra_serf__get_resource_type(&kind, props)); + + if (kind != svn_node_dir) + { + return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL, + _("Can't get entries of non-directory")); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra__vtable_t.get_dir(). */ +static svn_error_t * +svn_ra_serf__get_dir(svn_ra_session_t *ra_session, + apr_hash_t **dirents, + svn_revnum_t *fetched_rev, + apr_hash_t **ret_props, + const char *rel_path, + svn_revnum_t revision, + apr_uint32_t dirent_fields, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + const char *path; + + path = session->session_url.path; + + /* If we have a relative path, URI encode and append it. */ + if (rel_path) + { + path = svn_path_url_add_component2(path, rel_path, pool); + } + + /* If the user specified a peg revision other than HEAD, we have to fetch + the baseline collection url for that revision. If not, we can use the + public url. */ + if (SVN_IS_VALID_REVNUM(revision) || fetched_rev) + { + SVN_ERR(svn_ra_serf__get_stable_url(&path, fetched_rev, + session, NULL /* conn */, + path, revision, + pool, pool)); + revision = SVN_INVALID_REVNUM; + } + /* REVISION is always SVN_INVALID_REVNUM */ + SVN_ERR_ASSERT(!SVN_IS_VALID_REVNUM(revision)); + + /* If we're asked for children, fetch them now. */ + if (dirents) + { + struct path_dirent_visitor_t dirent_walk; + apr_hash_t *props; + const char *rtype; + + /* Always request node kind to check that path is really a + * directory. + */ + dirent_fields |= SVN_DIRENT_KIND; + SVN_ERR(svn_ra_serf__retrieve_props(&props, session, session->conns[0], + path, SVN_INVALID_REVNUM, "1", + get_dirent_props(dirent_fields, + session, pool), + pool, pool)); + + /* Check if the path is really a directory. */ + rtype = svn_ra_serf__get_prop(props, path, "DAV:", "resourcetype"); + if (rtype == NULL || strcmp(rtype, "collection") != 0) + return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL, + _("Can't get entries of non-directory")); + + /* We're going to create two hashes to help the walker along. + * We're going to return the 2nd one back to the caller as it + * will have the basenames it expects. + */ + dirent_walk.full_paths = apr_hash_make(pool); + dirent_walk.base_paths = apr_hash_make(pool); + dirent_walk.orig_path = svn_urlpath__canonicalize(path, pool); + dirent_walk.supports_deadprop_count = svn_tristate_unknown; + dirent_walk.result_pool = pool; + + SVN_ERR(svn_ra_serf__walk_all_paths(props, SVN_INVALID_REVNUM, + path_dirent_walker, &dirent_walk, + pool)); + + if (dirent_walk.supports_deadprop_count == svn_tristate_false + && session->supports_deadprop_count == svn_tristate_unknown + && dirent_fields & SVN_DIRENT_HAS_PROPS) + { + /* We have to requery as the server didn't give us the right + information */ + session->supports_deadprop_count = svn_tristate_false; + SVN_ERR(svn_ra_serf__retrieve_props(&props, session, + session->conns[0], + path, SVN_INVALID_REVNUM, "1", + get_dirent_props(dirent_fields, + session, pool), + pool, pool)); + + apr_hash_clear(dirent_walk.full_paths); + apr_hash_clear(dirent_walk.base_paths); + + SVN_ERR(svn_ra_serf__walk_all_paths(props, SVN_INVALID_REVNUM, + path_dirent_walker, + &dirent_walk, pool)); + } + + *dirents = dirent_walk.base_paths; + + if (dirent_walk.supports_deadprop_count != svn_tristate_unknown) + session->supports_deadprop_count = dirent_walk.supports_deadprop_count; + } + + /* If we're asked for the directory properties, fetch them too. */ + if (ret_props) + { + apr_hash_t *props; + + SVN_ERR(svn_ra_serf__fetch_node_props(&props, session->conns[0], + path, SVN_INVALID_REVNUM, + all_props, + pool, pool)); + + /* Check if the path is really a directory. */ + SVN_ERR(resource_is_directory(props)); + + /* ### flatten_props() does not copy PROPVALUE, but fetch_node_props() + ### put them into POOL, so we're okay. */ + SVN_ERR(svn_ra_serf__flatten_props(ret_props, props, pool, pool)); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_repos_root(svn_ra_session_t *ra_session, + const char **url, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + if (!session->repos_root_str) + { + const char *vcc_url; + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool)); + } + + *url = session->repos_root_str; + return SVN_NO_ERROR; +} + +/* TODO: to fetch the uuid from the repository, we need: + 1. a path that exists in HEAD + 2. a path that's readable + + get_uuid handles the case where a path doesn't exist in HEAD and also the + case where the root of the repository is not readable. + However, it does not handle the case where we're fetching path not existing + in HEAD of a repository with unreadable root directory. + + Implements svn_ra__vtable_t.get_uuid(). + */ +static svn_error_t * +svn_ra_serf__get_uuid(svn_ra_session_t *ra_session, + const char **uuid, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + if (!session->uuid) + { + const char *vcc_url; + + /* We should never get here if we have HTTP v2 support, because + any server with that support should be transmitting the + UUID in the initial OPTIONS response. */ + SVN_ERR_ASSERT(! SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)); + + /* We're not interested in vcc_url and relative_url, but this call also + stores the repository's uuid in the session. */ + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool)); + if (!session->uuid) + { + return svn_error_create(SVN_ERR_RA_DAV_RESPONSE_HEADER_BADNESS, NULL, + _("The UUID property was not found on the " + "resource or any of its parents")); + } + } + + *uuid = session->uuid; + + return SVN_NO_ERROR; +} + + +static const svn_ra__vtable_t serf_vtable = { + ra_serf_version, + ra_serf_get_description, + ra_serf_get_schemes, + svn_ra_serf__open, + svn_ra_serf__reparent, + svn_ra_serf__get_session_url, + svn_ra_serf__get_latest_revnum, + svn_ra_serf__get_dated_revision, + svn_ra_serf__change_rev_prop, + svn_ra_serf__rev_proplist, + svn_ra_serf__rev_prop, + svn_ra_serf__get_commit_editor, + svn_ra_serf__get_file, + svn_ra_serf__get_dir, + svn_ra_serf__get_mergeinfo, + svn_ra_serf__do_update, + svn_ra_serf__do_switch, + svn_ra_serf__do_status, + svn_ra_serf__do_diff, + svn_ra_serf__get_log, + svn_ra_serf__check_path, + svn_ra_serf__stat, + svn_ra_serf__get_uuid, + svn_ra_serf__get_repos_root, + svn_ra_serf__get_locations, + svn_ra_serf__get_location_segments, + svn_ra_serf__get_file_revs, + svn_ra_serf__lock, + svn_ra_serf__unlock, + svn_ra_serf__get_lock, + svn_ra_serf__get_locks, + svn_ra_serf__replay, + svn_ra_serf__has_capability, + svn_ra_serf__replay_range, + svn_ra_serf__get_deleted_rev, + svn_ra_serf__register_editor_shim_callbacks, + svn_ra_serf__get_inherited_props +}; + +svn_error_t * +svn_ra_serf__init(const svn_version_t *loader_version, + const svn_ra__vtable_t **vtable, + apr_pool_t *pool) +{ + static const svn_version_checklist_t checklist[] = + { + { "svn_subr", svn_subr_version }, + { "svn_delta", svn_delta_version }, + { NULL, NULL } + }; + int serf_major; + int serf_minor; + int serf_patch; + + SVN_ERR(svn_ver_check_list(ra_serf_version(), checklist)); + + /* Simplified version check to make sure we can safely use the + VTABLE parameter. The RA loader does a more exhaustive check. */ + if (loader_version->major != SVN_VER_MAJOR) + { + return svn_error_createf( + SVN_ERR_VERSION_MISMATCH, NULL, + _("Unsupported RA loader version (%d) for ra_serf"), + loader_version->major); + } + + /* Make sure that we have loaded a compatible library: the MAJOR must + match, and the minor must be at *least* what we compiled against. + The patch level is simply ignored. */ + serf_lib_version(&serf_major, &serf_minor, &serf_patch); + if (serf_major != SERF_MAJOR_VERSION + || serf_minor < SERF_MINOR_VERSION) + { + return svn_error_createf( + /* ### should return a unique error */ + SVN_ERR_VERSION_MISMATCH, NULL, + _("ra_serf was compiled for serf %d.%d.%d but loaded " + "an incompatible %d.%d.%d library"), + SERF_MAJOR_VERSION, SERF_MINOR_VERSION, SERF_PATCH_VERSION, + serf_major, serf_minor, serf_patch); + } + + *vtable = &serf_vtable; + + return SVN_NO_ERROR; +} + +/* Compatibility wrapper for pre-1.2 subversions. Needed? */ +#define NAME "ra_serf" +#define DESCRIPTION RA_SERF_DESCRIPTION +#define VTBL serf_vtable +#define INITFUNC svn_ra_serf__init +#define COMPAT_INITFUNC svn_ra_serf_init +#include "../libsvn_ra/wrapper_template.h" diff --git a/subversion/libsvn_ra_serf/update.c b/subversion/libsvn_ra_serf/update.c new file mode 100644 index 0000000..21ed2df --- /dev/null +++ b/subversion/libsvn_ra_serf/update.c @@ -0,0 +1,3639 @@ +/* + * update.c : entry point for update RA functions for ra_serf + * + * ==================================================================== + * 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. + * ==================================================================== + */ + + + +#define APR_WANT_STRFUNC +#include <apr_version.h> +#include <apr_want.h> + +#include <apr_uri.h> + +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "svn_delta.h" +#include "svn_path.h" +#include "svn_base64.h" +#include "svn_props.h" + +#include "svn_private_config.h" +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_string_private.h" + +#include "ra_serf.h" +#include "../libsvn_ra/ra_loader.h" + + +/* + * This enum represents the current state of our XML parsing for a REPORT. + * + * A little explanation of how the parsing works. Every time we see + * an open-directory tag, we enter the OPEN_DIR state. Likewise, for + * add-directory, open-file, etc. When we see the closing variant of the + * open-directory tag, we'll 'pop' out of that state. + * + * Each state has a pool associated with it that can have temporary + * allocations that will live as long as the tag is opened. Once + * the tag is 'closed', the pool will be reused. + */ +typedef enum report_state_e { + NONE = 0, + INITIAL = 0, + UPDATE_REPORT, + TARGET_REVISION, + OPEN_DIR, + ADD_DIR, + ABSENT_DIR, + OPEN_FILE, + ADD_FILE, + ABSENT_FILE, + PROP, + IGNORE_PROP_NAME, + NEED_PROP_NAME, + TXDELTA +} report_state_e; + + +/* While we process the REPORT response, we will queue up GET and PROPFIND + requests. For a very large checkout, it is very easy to queue requests + faster than they are resolved. Thus, we need to pause the XML processing + (which queues more requests) to avoid queueing too many, with their + attendant memory costs. When the queue count drops low enough, we will + resume XML processing. + + Note that we don't want the count to drop to zero. We have multiple + connections that we want to keep busy. These are also heuristic numbers + since network and parsing behavior (ie. it doesn't pause immediately) + can make the measurements quite imprecise. + + We measure outstanding requests as the sum of NUM_ACTIVE_FETCHES and + NUM_ACTIVE_PROPFINDS in the report_context_t structure. */ +#define REQUEST_COUNT_TO_PAUSE 50 +#define REQUEST_COUNT_TO_RESUME 40 + + +/* Forward-declare our report context. */ +typedef struct report_context_t report_context_t; + +/* + * This structure represents the information for a directory. + */ +typedef struct report_dir_t +{ + /* Our parent directory. + * + * This value is NULL when we are the root. + */ + struct report_dir_t *parent_dir; + + apr_pool_t *pool; + + /* Pointer back to our original report context. */ + report_context_t *report_context; + + /* Our name sans any parents. */ + const char *base_name; + + /* the expanded directory name (including all parent names) */ + const char *name; + + /* the canonical url for this directory after updating. (received) */ + const char *url; + + /* The original repos_relpath of this url (from the working copy) + or NULL if the repos_relpath can be calculated from the edit root. */ + const char *repos_relpath; + + /* Our base revision - SVN_INVALID_REVNUM if we're adding this dir. */ + svn_revnum_t base_rev; + + /* controlling dir baton - this is only created in ensure_dir_opened() */ + void *dir_baton; + apr_pool_t *dir_baton_pool; + + /* How many references to this directory do we still have open? */ + apr_size_t ref_count; + + /* Namespace list allocated out of this ->pool. */ + svn_ra_serf__ns_t *ns_list; + + /* hashtable for all of the properties (shared within a dir) */ + apr_hash_t *props; + + /* hashtable for all to-be-removed properties (shared within a dir) */ + apr_hash_t *removed_props; + + /* The propfind request for our current directory */ + svn_ra_serf__handler_t *propfind_handler; + + /* Has the server told us to fetch the dir props? */ + svn_boolean_t fetch_props; + + /* Have we closed the directory tag (meaning no more additions)? */ + svn_boolean_t tag_closed; + + /* The children of this directory */ + struct report_dir_t *children; + + /* The next sibling of this directory */ + struct report_dir_t *sibling; +} report_dir_t; + +/* + * This structure represents the information for a file. + * + * A directory may have a report_info_t associated with it as well. + * + * This structure is created as we parse the REPORT response and + * once the element is completed, we create a report_fetch_t structure + * to give to serf to retrieve this file. + */ +typedef struct report_info_t +{ + apr_pool_t *pool; + + /* The enclosing directory. + * + * If this structure refers to a directory, the dir it points to will be + * itself. + */ + report_dir_t *dir; + + /* Our name sans any directory info. */ + const char *base_name; + + /* the expanded file name (including all parent directory names) */ + const char *name; + + /* the canonical url for this file. */ + const char *url; + + /* lock token, if we had one to start off with. */ + const char *lock_token; + + /* Our base revision - SVN_INVALID_REVNUM if we're adding this file. */ + svn_revnum_t base_rev; + + /* our delta base, if present (NULL if we're adding the file) */ + const char *delta_base; + + /* Path of original item if add with history */ + const char *copyfrom_path; + + /* Revision of original item if add with history */ + svn_revnum_t copyfrom_rev; + + /* The propfind request for our current file (if present) */ + svn_ra_serf__handler_t *propfind_handler; + + /* Has the server told us to fetch the file props? */ + svn_boolean_t fetch_props; + + /* Has the server told us to go fetch - only valid if we had it already */ + svn_boolean_t fetch_file; + + /* The properties for this file */ + apr_hash_t *props; + + /* pool passed to update->add_file, etc. */ + apr_pool_t *editor_pool; + + /* controlling file_baton and textdelta handler */ + void *file_baton; + const char *base_checksum; + const char *final_sha1_checksum; + svn_txdelta_window_handler_t textdelta; + void *textdelta_baton; + svn_stream_t *svndiff_decoder; + svn_stream_t *base64_decoder; + + /* Checksum for close_file */ + const char *final_checksum; + + /* Stream containing file contents already cached in the working + copy (which may be used to avoid a GET request for the same). */ + svn_stream_t *cached_contents; + + /* temporary property for this file which is currently being parsed + * It will eventually be stored in our parent directory's property hash. + */ + const char *prop_ns; + const char *prop_name; + svn_stringbuf_t *prop_value; + const char *prop_encoding; +} report_info_t; + +/* + * This structure represents a single request to GET (fetch) a file with + * its associated Serf session/connection. + */ +typedef struct report_fetch_t { + + /* The handler representing this particular fetch. */ + svn_ra_serf__handler_t *handler; + + /* The session we should use to fetch the file. */ + svn_ra_serf__session_t *sess; + + /* The connection we should use to fetch file. */ + svn_ra_serf__connection_t *conn; + + /* Stores the information for the file we want to fetch. */ + report_info_t *info; + + /* Have we read our response headers yet? */ + svn_boolean_t read_headers; + + /* This flag is set when our response is aborted before we reach the + * end and we decide to requeue this request. + */ + svn_boolean_t aborted_read; + apr_off_t aborted_read_size; + + /* This is the amount of data that we have read so far. */ + apr_off_t read_size; + + /* If we're receiving an svndiff, this will be non-NULL. */ + svn_stream_t *delta_stream; + + /* If we're writing this file to a stream, this will be non-NULL. */ + svn_stream_t *target_stream; + + /* Are we done fetching this file? */ + svn_boolean_t done; + + /* Discard the rest of the content? */ + svn_boolean_t discard; + + svn_ra_serf__list_t **done_list; + svn_ra_serf__list_t done_item; + +} report_fetch_t; + +/* + * The master structure for a REPORT request and response. + */ +struct report_context_t { + apr_pool_t *pool; + + svn_ra_serf__session_t *sess; + svn_ra_serf__connection_t *conn; + + /* Source path and destination path */ + const char *source; + const char *destination; + + /* Our update target. */ + const char *update_target; + + /* What is the target revision that we want for this REPORT? */ + svn_revnum_t target_rev; + + /* Have we been asked to ignore ancestry or textdeltas? */ + svn_boolean_t ignore_ancestry; + svn_boolean_t text_deltas; + + /* Do we want the server to send copyfrom args or not? */ + svn_boolean_t send_copyfrom_args; + + /* Is the server sending everything in one response? */ + svn_boolean_t send_all_mode; + + /* Is the server including properties inline for newly added + files/dirs? */ + svn_boolean_t add_props_included; + + /* Path -> lock token mapping. */ + apr_hash_t *lock_path_tokens; + + /* Path -> const char *repos_relpath mapping */ + apr_hash_t *switched_paths; + + /* Boolean indicating whether "" is switched. + (This indicates that the we are updating a single file) */ + svn_boolean_t root_is_switched; + + /* Our master update editor and baton. */ + const svn_delta_editor_t *update_editor; + void *update_baton; + + /* The file holding request body for the REPORT. + * + * ### todo: It will be better for performance to store small + * request bodies (like 4k) in memory and bigger bodies on disk. + */ + apr_file_t *body_file; + + /* root directory object */ + report_dir_t *root_dir; + + /* number of pending GET requests */ + unsigned int num_active_fetches; + + /* completed fetches (contains report_fetch_t) */ + svn_ra_serf__list_t *done_fetches; + + /* number of pending PROPFIND requests */ + unsigned int num_active_propfinds; + + /* completed PROPFIND requests (contains svn_ra_serf__handler_t) */ + svn_ra_serf__list_t *done_propfinds; + svn_ra_serf__list_t *done_dir_propfinds; + + /* list of outstanding prop changes (contains report_dir_t) */ + svn_ra_serf__list_t *active_dir_propfinds; + + /* list of files that only have prop changes (contains report_info_t) */ + svn_ra_serf__list_t *file_propchanges_only; + + /* The path to the REPORT request */ + const char *path; + + /* Are we done parsing the REPORT response? */ + svn_boolean_t done; + + /* Did we receive all data from the network? */ + svn_boolean_t report_received; + + /* Did we get a complete (non-truncated) report? */ + svn_boolean_t report_completed; + + /* The XML parser context for the REPORT response. */ + svn_ra_serf__xml_parser_t *parser_ctx; + + /* Did we close the root directory? */ + svn_boolean_t closed_root; +}; + + +#ifdef NOT_USED_YET + +#define D_ "DAV:" +#define S_ SVN_XML_NAMESPACE +static const svn_ra_serf__xml_transition_t update_ttable[] = { + { INITIAL, S_, "update-report", UPDATE_REPORT, + FALSE, { NULL }, FALSE }, + + { UPDATE_REPORT, S_, "target-revision", TARGET_REVISION, + FALSE, { "rev", NULL }, TRUE }, + + { UPDATE_REPORT, S_, "open-directory", OPEN_DIR, + FALSE, { "rev", NULL }, TRUE }, + + { OPEN_DIR, S_, "open-directory", OPEN_DIR, + FALSE, { "rev", "name", NULL }, TRUE }, + + { OPEN_DIR, S_, "add-directory", ADD_DIR, + FALSE, { "rev", "name", "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { ADD_DIR, S_, "add-directory", ADD_DIR, + FALSE, { "rev", "name", "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { OPEN_DIR, S_, "open-file", OPEN_FILE, + FALSE, { "rev", "name", NULL }, TRUE }, + + { OPEN_DIR, S_, "add-file", ADD_FILE, + FALSE, { "rev", "name", "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { ADD_DIR, S_, "add-file", ADD_FILE, + FALSE, { "rev", "name", "?copyfrom-path", "?copyfrom-rev", NULL }, TRUE }, + + { OPEN_DIR, S_, "delete-entry", OPEN_FILE, + FALSE, { "?rev", "name", NULL }, TRUE }, + + { OPEN_DIR, S_, "absent-directory", ABSENT_DIR, + FALSE, { "name", NULL }, TRUE }, + + { ADD_DIR, S_, "absent-directory", ABSENT_DIR, + FALSE, { "name", NULL }, TRUE }, + + { OPEN_DIR, S_, "absent-file", ABSENT_FILE, + FALSE, { "name", NULL }, TRUE }, + + { ADD_DIR, S_, "absent-file", ABSENT_FILE, + FALSE, { "name", NULL }, TRUE }, + + { 0 } +}; + + + +/* Conforms to svn_ra_serf__xml_opened_t */ +static svn_error_t * +update_opened(svn_ra_serf__xml_estate_t *xes, + void *baton, + int entered_state, + const svn_ra_serf__dav_props_t *tag, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = baton; + + return SVN_NO_ERROR; +} + + + +/* Conforms to svn_ra_serf__xml_closed_t */ +static svn_error_t * +update_closed(svn_ra_serf__xml_estate_t *xes, + void *baton, + int leaving_state, + const svn_string_t *cdata, + apr_hash_t *attrs, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = baton; + + if (leaving_state == TARGET_REVISION) + { + const char *rev = svn_hash_gets(attrs, "rev"); + + SVN_ERR(ctx->update_editor->set_target_revision(ctx->update_baton, + SVN_STR_TO_REV(rev), + ctx->sess->pool)); + } + + return SVN_NO_ERROR; +} + + +/* Conforms to svn_ra_serf__xml_cdata_t */ +static svn_error_t * +update_cdata(svn_ra_serf__xml_estate_t *xes, + void *baton, + int current_state, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = baton; + + return SVN_NO_ERROR; +} + +#endif /* NOT_USED_YET */ + + +/* Returns best connection for fetching files/properties. */ +static svn_ra_serf__connection_t * +get_best_connection(report_context_t *ctx) +{ + svn_ra_serf__connection_t *conn; + int first_conn = 1; + + /* Skip the first connection if the REPORT response hasn't been completely + received yet or if we're being told to limit our connections to + 2 (because this could be an attempt to ensure that we do all our + auxiliary GETs/PROPFINDs on a single connection). + + ### FIXME: This latter requirement (max_connections > 2) is + ### really just a hack to work around the fact that some update + ### editor implementations (such as svnrdump's dump editor) + ### simply can't handle the way ra_serf violates the editor v1 + ### drive ordering requirements. + ### + ### See http://subversion.tigris.org/issues/show_bug.cgi?id=4116. + */ + if (ctx->report_received && (ctx->sess->max_connections > 2)) + first_conn = 0; + + /* Currently, we just cycle connections. In the future we could + store the number of pending requests on each connection, or + perform other heuristics, to achieve better connection usage. + (As an optimization, if there's only one available auxiliary + connection to use, don't bother doing all the cur_conn math -- + just return that one connection.) */ + if (ctx->sess->num_conns - first_conn == 1) + { + conn = ctx->sess->conns[first_conn]; + } + else + { + conn = ctx->sess->conns[ctx->sess->cur_conn]; + ctx->sess->cur_conn++; + if (ctx->sess->cur_conn >= ctx->sess->num_conns) + ctx->sess->cur_conn = first_conn; + } + return conn; +} + + +/** Report state management helper **/ + +static report_info_t * +push_state(svn_ra_serf__xml_parser_t *parser, + report_context_t *ctx, + report_state_e state) +{ + report_info_t *info; + apr_pool_t *info_parent_pool; + + svn_ra_serf__xml_push_state(parser, state); + + info = parser->state->private; + + /* Our private pool needs to be disjoint from the state pool. */ + if (!info) + { + info_parent_pool = ctx->pool; + } + else + { + info_parent_pool = info->pool; + } + + if (state == OPEN_DIR || state == ADD_DIR) + { + report_info_t *new_info; + + new_info = apr_pcalloc(info_parent_pool, sizeof(*new_info)); + new_info->pool = svn_pool_create(info_parent_pool); + new_info->lock_token = NULL; + new_info->prop_value = svn_stringbuf_create_empty(new_info->pool); + + new_info->dir = apr_pcalloc(new_info->pool, sizeof(*new_info->dir)); + new_info->dir->pool = new_info->pool; + + /* Create the root property tree. */ + new_info->dir->props = apr_hash_make(new_info->pool); + new_info->props = new_info->dir->props; + new_info->dir->removed_props = apr_hash_make(new_info->pool); + + new_info->dir->report_context = ctx; + + if (info) + { + info->dir->ref_count++; + + new_info->dir->parent_dir = info->dir; + + /* Point our ns_list at our parents to try to reuse it. */ + new_info->dir->ns_list = info->dir->ns_list; + + /* Add ourselves to our parent's list */ + new_info->dir->sibling = info->dir->children; + info->dir->children = new_info->dir; + } + else + { + /* Allow us to be found later. */ + ctx->root_dir = new_info->dir; + } + + parser->state->private = new_info; + } + else if (state == OPEN_FILE || state == ADD_FILE) + { + report_info_t *new_info; + + new_info = apr_pcalloc(info_parent_pool, sizeof(*new_info)); + new_info->pool = svn_pool_create(info_parent_pool); + new_info->file_baton = NULL; + new_info->lock_token = NULL; + new_info->fetch_file = FALSE; + new_info->prop_value = svn_stringbuf_create_empty(new_info->pool); + + /* Point at our parent's directory state. */ + new_info->dir = info->dir; + info->dir->ref_count++; + + new_info->props = apr_hash_make(new_info->pool); + + parser->state->private = new_info; + } + + return parser->state->private; +} + + +/** Wrappers around our various property walkers **/ + +static svn_error_t * +set_file_props(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + report_info_t *info = baton; + const svn_delta_editor_t *editor = info->dir->report_context->update_editor; + const char *prop_name; + + prop_name = svn_ra_serf__svnname_from_wirename(ns, name, scratch_pool); + if (prop_name != NULL) + return svn_error_trace(editor->change_file_prop(info->file_baton, + prop_name, + val, + scratch_pool)); + return SVN_NO_ERROR; +} + + +static svn_error_t * +set_dir_props(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + report_dir_t *dir = baton; + const svn_delta_editor_t *editor = dir->report_context->update_editor; + const char *prop_name; + + prop_name = svn_ra_serf__svnname_from_wirename(ns, name, scratch_pool); + if (prop_name != NULL) + return svn_error_trace(editor->change_dir_prop(dir->dir_baton, + prop_name, + val, + scratch_pool)); + return SVN_NO_ERROR; +} + + +static svn_error_t * +remove_file_props(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + report_info_t *info = baton; + const svn_delta_editor_t *editor = info->dir->report_context->update_editor; + const char *prop_name; + + prop_name = svn_ra_serf__svnname_from_wirename(ns, name, scratch_pool); + if (prop_name != NULL) + return svn_error_trace(editor->change_file_prop(info->file_baton, + prop_name, + NULL, + scratch_pool)); + return SVN_NO_ERROR; +} + + +static svn_error_t * +remove_dir_props(void *baton, + const char *ns, + const char *name, + const svn_string_t *val, + apr_pool_t *scratch_pool) +{ + report_dir_t *dir = baton; + const svn_delta_editor_t *editor = dir->report_context->update_editor; + const char *prop_name; + + prop_name = svn_ra_serf__svnname_from_wirename(ns, name, scratch_pool); + if (prop_name != NULL) + return svn_error_trace(editor->change_dir_prop(dir->dir_baton, + prop_name, + NULL, + scratch_pool)); + return SVN_NO_ERROR; +} + + +/** Helpers to open and close directories */ + +static svn_error_t* +ensure_dir_opened(report_dir_t *dir) +{ + report_context_t *ctx = dir->report_context; + + /* if we're already open, return now */ + if (dir->dir_baton) + { + return SVN_NO_ERROR; + } + + if (dir->base_name[0] == '\0') + { + dir->dir_baton_pool = svn_pool_create(dir->pool); + + if (ctx->destination + && ctx->sess->wc_callbacks->invalidate_wc_props) + { + SVN_ERR(ctx->sess->wc_callbacks->invalidate_wc_props( + ctx->sess->wc_callback_baton, + ctx->update_target, + SVN_RA_SERF__WC_CHECKED_IN_URL, dir->pool)); + } + + SVN_ERR(ctx->update_editor->open_root(ctx->update_baton, dir->base_rev, + dir->dir_baton_pool, + &dir->dir_baton)); + } + else + { + SVN_ERR(ensure_dir_opened(dir->parent_dir)); + + dir->dir_baton_pool = svn_pool_create(dir->parent_dir->dir_baton_pool); + + if (SVN_IS_VALID_REVNUM(dir->base_rev)) + { + SVN_ERR(ctx->update_editor->open_directory(dir->name, + dir->parent_dir->dir_baton, + dir->base_rev, + dir->dir_baton_pool, + &dir->dir_baton)); + } + else + { + SVN_ERR(ctx->update_editor->add_directory(dir->name, + dir->parent_dir->dir_baton, + NULL, SVN_INVALID_REVNUM, + dir->dir_baton_pool, + &dir->dir_baton)); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +close_dir(report_dir_t *dir) +{ + report_dir_t *prev; + report_dir_t *sibling; + + /* ### is there a better pool... this is tossed at end-of-func */ + apr_pool_t *scratch_pool = dir->dir_baton_pool; + + SVN_ERR_ASSERT(! dir->ref_count); + + SVN_ERR(svn_ra_serf__walk_all_props(dir->props, dir->base_name, + dir->base_rev, + set_dir_props, dir, + scratch_pool)); + + SVN_ERR(svn_ra_serf__walk_all_props(dir->removed_props, dir->base_name, + dir->base_rev, remove_dir_props, dir, + scratch_pool)); + + if (dir->fetch_props) + { + SVN_ERR(svn_ra_serf__walk_all_props(dir->props, dir->url, + dir->report_context->target_rev, + set_dir_props, dir, + scratch_pool)); + } + + SVN_ERR(dir->report_context->update_editor->close_directory( + dir->dir_baton, scratch_pool)); + + /* remove us from our parent's children list */ + if (dir->parent_dir) + { + prev = NULL; + sibling = dir->parent_dir->children; + + while (sibling != dir) + { + prev = sibling; + sibling = sibling->sibling; + if (!sibling) + SVN_ERR_MALFUNCTION(); + } + + if (!prev) + { + dir->parent_dir->children = dir->sibling; + } + else + { + prev->sibling = dir->sibling; + } + } + + svn_pool_destroy(dir->dir_baton_pool); + svn_pool_destroy(dir->pool); + + return SVN_NO_ERROR; +} + +static svn_error_t *close_all_dirs(report_dir_t *dir) +{ + while (dir->children) + { + SVN_ERR(close_all_dirs(dir->children)); + dir->ref_count--; + } + + SVN_ERR_ASSERT(! dir->ref_count); + + SVN_ERR(ensure_dir_opened(dir)); + + return close_dir(dir); +} + + +/** Routines called when we are fetching a file */ + +/* This function works around a bug in some older versions of + * mod_dav_svn in that it will not send remove-prop in the update + * report when a lock property disappears when send-all is false. + * + * Therefore, we'll try to look at our properties and see if there's + * an active lock. If not, then we'll assume there isn't a lock + * anymore. + */ +static void +check_lock(report_info_t *info) +{ + const char *lock_val; + + lock_val = svn_ra_serf__get_ver_prop(info->props, info->url, + info->dir->report_context->target_rev, + "DAV:", "lockdiscovery"); + + if (lock_val) + { + char *new_lock; + new_lock = apr_pstrdup(info->editor_pool, lock_val); + apr_collapse_spaces(new_lock, new_lock); + lock_val = new_lock; + } + + if (!lock_val || lock_val[0] == '\0') + { + svn_string_t *str; + + str = svn_string_ncreate("", 1, info->editor_pool); + + svn_ra_serf__set_ver_prop(info->dir->removed_props, info->base_name, + info->base_rev, "DAV:", "lock-token", + str, info->dir->pool); + } +} + +static svn_error_t * +headers_fetch(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + report_fetch_t *fetch_ctx = baton; + + /* note that we have old VC URL */ + if (SVN_IS_VALID_REVNUM(fetch_ctx->info->base_rev) && + fetch_ctx->info->delta_base) + { + serf_bucket_headers_setn(headers, SVN_DAV_DELTA_BASE_HEADER, + fetch_ctx->info->delta_base); + serf_bucket_headers_setn(headers, "Accept-Encoding", + "svndiff1;q=0.9,svndiff;q=0.8"); + } + else if (fetch_ctx->sess->using_compression) + { + serf_bucket_headers_setn(headers, "Accept-Encoding", "gzip"); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +cancel_fetch(serf_request_t *request, + serf_bucket_t *response, + int status_code, + void *baton) +{ + report_fetch_t *fetch_ctx = baton; + + /* Uh-oh. Our connection died on us. + * + * The core ra_serf layer will requeue our request - we just need to note + * that we got cut off in the middle of our song. + */ + if (!response) + { + /* If we already started the fetch and opened the file handle, we need + * to hold subsequent read() ops until we get back to where we were + * before the close and we can then resume the textdelta() calls. + */ + if (fetch_ctx->read_headers) + { + if (!fetch_ctx->aborted_read && fetch_ctx->read_size) + { + fetch_ctx->aborted_read = TRUE; + fetch_ctx->aborted_read_size = fetch_ctx->read_size; + } + fetch_ctx->read_size = 0; + } + + return SVN_NO_ERROR; + } + + /* We have no idea what went wrong. */ + SVN_ERR_MALFUNCTION(); +} + +static svn_error_t * +error_fetch(serf_request_t *request, + report_fetch_t *fetch_ctx, + svn_error_t *err) +{ + fetch_ctx->done = TRUE; + + fetch_ctx->done_item.data = fetch_ctx; + fetch_ctx->done_item.next = *fetch_ctx->done_list; + *fetch_ctx->done_list = &fetch_ctx->done_item; + + /* Discard the rest of this request + (This makes sure it doesn't error when the request is aborted later) */ + serf_request_set_handler(request, + svn_ra_serf__response_discard_handler, NULL); + + /* Some errors would be handled by serf; make sure they really make + the update fail by wrapping it in a different error. */ + if (!SERF_BUCKET_READ_ERROR(err->apr_err)) + return svn_error_create(SVN_ERR_RA_SERF_WRAPPED_ERROR, err, NULL); + + return err; +} + +/* Wield the editor referenced by INFO to open (or add) the file + file also associated with INFO, setting properties on the file and + calling the editor's apply_textdelta() function on it if necessary + (or if FORCE_APPLY_TEXTDELTA is set). + + Callers will probably want to also see the function that serves + the opposite purpose of this one, close_updated_file(). */ +static svn_error_t * +open_updated_file(report_info_t *info, + svn_boolean_t force_apply_textdelta, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = info->dir->report_context; + const svn_delta_editor_t *update_editor = ctx->update_editor; + + /* Ensure our parent is open. */ + SVN_ERR(ensure_dir_opened(info->dir)); + info->editor_pool = svn_pool_create(info->dir->dir_baton_pool); + + /* Expand our full name now if we haven't done so yet. */ + if (!info->name) + { + info->name = svn_relpath_join(info->dir->name, info->base_name, + info->editor_pool); + } + + /* Open (or add) the file. */ + if (SVN_IS_VALID_REVNUM(info->base_rev)) + { + SVN_ERR(update_editor->open_file(info->name, + info->dir->dir_baton, + info->base_rev, + info->editor_pool, + &info->file_baton)); + } + else + { + SVN_ERR(update_editor->add_file(info->name, + info->dir->dir_baton, + info->copyfrom_path, + info->copyfrom_rev, + info->editor_pool, + &info->file_baton)); + } + + /* Check for lock information. */ + if (info->lock_token) + check_lock(info); + + /* Get (maybe) a textdelta window handler for transmitting file + content changes. */ + if (info->fetch_file || force_apply_textdelta) + { + SVN_ERR(update_editor->apply_textdelta(info->file_baton, + info->base_checksum, + info->editor_pool, + &info->textdelta, + &info->textdelta_baton)); + } + + return SVN_NO_ERROR; +} + +/* Close the file associated with INFO->file_baton, and cleanup other + bits of that structure managed by open_updated_file(). */ +static svn_error_t * +close_updated_file(report_info_t *info, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = info->dir->report_context; + + /* Set all of the properties we received */ + SVN_ERR(svn_ra_serf__walk_all_props(info->props, + info->base_name, + info->base_rev, + set_file_props, info, + scratch_pool)); + SVN_ERR(svn_ra_serf__walk_all_props(info->dir->removed_props, + info->base_name, + info->base_rev, + remove_file_props, info, + scratch_pool)); + if (info->fetch_props) + { + SVN_ERR(svn_ra_serf__walk_all_props(info->props, + info->url, + ctx->target_rev, + set_file_props, info, + scratch_pool)); + } + + /* Close the file via the editor. */ + SVN_ERR(info->dir->report_context->update_editor->close_file( + info->file_baton, info->final_checksum, scratch_pool)); + + /* We're done with our editor pool. */ + svn_pool_destroy(info->editor_pool); + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +handle_fetch(serf_request_t *request, + serf_bucket_t *response, + void *handler_baton, + apr_pool_t *pool) +{ + const char *data; + apr_size_t len; + apr_status_t status; + report_fetch_t *fetch_ctx = handler_baton; + svn_error_t *err; + + /* ### new field. make sure we didn't miss some initialization. */ + SVN_ERR_ASSERT(fetch_ctx->handler != NULL); + + if (!fetch_ctx->read_headers) + { + serf_bucket_t *hdrs; + const char *val; + report_info_t *info; + + hdrs = serf_bucket_response_get_headers(response); + val = serf_bucket_headers_get(hdrs, "Content-Type"); + info = fetch_ctx->info; + + if (val && svn_cstring_casecmp(val, SVN_SVNDIFF_MIME_TYPE) == 0) + { + fetch_ctx->delta_stream = + svn_txdelta_parse_svndiff(info->textdelta, + info->textdelta_baton, + TRUE, info->editor_pool); + + /* Validate the delta base claimed by the server matches + what we asked for! */ + val = serf_bucket_headers_get(hdrs, SVN_DAV_DELTA_BASE_HEADER); + if (val && (strcmp(val, info->delta_base) != 0)) + { + err = svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("GET request returned unexpected " + "delta base: %s"), val); + return error_fetch(request, fetch_ctx, err); + } + } + else + { + fetch_ctx->delta_stream = NULL; + } + + fetch_ctx->read_headers = TRUE; + } + + /* If the error code wasn't 200, something went wrong. Don't use the returned + data as its probably an error message. Just bail out instead. */ + if (fetch_ctx->handler->sline.code != 200) + { + err = svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, NULL, + _("GET request failed: %d %s"), + fetch_ctx->handler->sline.code, + fetch_ctx->handler->sline.reason); + return error_fetch(request, fetch_ctx, err); + } + + while (1) + { + svn_txdelta_window_t delta_window = { 0 }; + svn_txdelta_op_t delta_op; + svn_string_t window_data; + + status = serf_bucket_read(response, 8000, &data, &len); + if (SERF_BUCKET_READ_ERROR(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + + fetch_ctx->read_size += len; + + if (fetch_ctx->aborted_read) + { + apr_off_t skip; + /* We haven't caught up to where we were before. */ + if (fetch_ctx->read_size < fetch_ctx->aborted_read_size) + { + /* Eek. What did the file shrink or something? */ + if (APR_STATUS_IS_EOF(status)) + { + SVN_ERR_MALFUNCTION(); + } + + /* Skip on to the next iteration of this loop. */ + if (APR_STATUS_IS_EAGAIN(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + continue; + } + + /* Woo-hoo. We're back. */ + fetch_ctx->aborted_read = FALSE; + + /* Update data and len to just provide the new data. */ + skip = len - (fetch_ctx->read_size - fetch_ctx->aborted_read_size); + data += skip; + len -= skip; + } + + if (fetch_ctx->delta_stream) + { + err = svn_stream_write(fetch_ctx->delta_stream, data, &len); + if (err) + { + return error_fetch(request, fetch_ctx, err); + } + } + /* otherwise, manually construct the text delta window. */ + else if (len) + { + window_data.data = data; + window_data.len = len; + + delta_op.action_code = svn_txdelta_new; + delta_op.offset = 0; + delta_op.length = len; + + delta_window.tview_len = len; + delta_window.num_ops = 1; + delta_window.ops = &delta_op; + delta_window.new_data = &window_data; + + /* write to the file located in the info. */ + err = fetch_ctx->info->textdelta(&delta_window, + fetch_ctx->info->textdelta_baton); + if (err) + { + return error_fetch(request, fetch_ctx, err); + } + } + + if (APR_STATUS_IS_EOF(status)) + { + report_info_t *info = fetch_ctx->info; + + if (fetch_ctx->delta_stream) + err = svn_error_trace(svn_stream_close(fetch_ctx->delta_stream)); + else + err = svn_error_trace(info->textdelta(NULL, + info->textdelta_baton)); + if (err) + { + return error_fetch(request, fetch_ctx, err); + } + + err = close_updated_file(info, info->pool); + if (err) + { + return svn_error_trace(error_fetch(request, fetch_ctx, err)); + } + + fetch_ctx->done = TRUE; + + fetch_ctx->done_item.data = fetch_ctx; + fetch_ctx->done_item.next = *fetch_ctx->done_list; + *fetch_ctx->done_list = &fetch_ctx->done_item; + + /* We're done with our pool. */ + svn_pool_destroy(info->pool); + + if (status) + return svn_ra_serf__wrap_err(status, NULL); + } + if (APR_STATUS_IS_EAGAIN(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + } + /* not reached */ +} + +/* Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +handle_stream(serf_request_t *request, + serf_bucket_t *response, + void *handler_baton, + apr_pool_t *pool) +{ + report_fetch_t *fetch_ctx = handler_baton; + svn_error_t *err; + apr_status_t status; + + /* ### new field. make sure we didn't miss some initialization. */ + SVN_ERR_ASSERT(fetch_ctx->handler != NULL); + + err = svn_ra_serf__error_on_status(fetch_ctx->handler->sline.code, + fetch_ctx->info->name, + fetch_ctx->handler->location); + if (err) + { + fetch_ctx->handler->done = TRUE; + + err = svn_error_compose_create( + err, + svn_ra_serf__handle_discard_body(request, response, NULL, pool)); + + return svn_error_trace(err); + } + + while (1) + { + const char *data; + apr_size_t len; + + status = serf_bucket_read(response, 8000, &data, &len); + if (SERF_BUCKET_READ_ERROR(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + + fetch_ctx->read_size += len; + + if (fetch_ctx->aborted_read) + { + /* We haven't caught up to where we were before. */ + if (fetch_ctx->read_size < fetch_ctx->aborted_read_size) + { + /* Eek. What did the file shrink or something? */ + if (APR_STATUS_IS_EOF(status)) + { + SVN_ERR_MALFUNCTION(); + } + + /* Skip on to the next iteration of this loop. */ + if (APR_STATUS_IS_EAGAIN(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + continue; + } + + /* Woo-hoo. We're back. */ + fetch_ctx->aborted_read = FALSE; + + /* Increment data and len by the difference. */ + data += fetch_ctx->read_size - fetch_ctx->aborted_read_size; + len += fetch_ctx->read_size - fetch_ctx->aborted_read_size; + } + + if (len) + { + apr_size_t written_len; + + written_len = len; + + SVN_ERR(svn_stream_write(fetch_ctx->target_stream, data, + &written_len)); + } + + if (APR_STATUS_IS_EOF(status)) + { + fetch_ctx->done = TRUE; + } + + if (status) + { + return svn_ra_serf__wrap_err(status, NULL); + } + } + /* not reached */ +} + +/* Close the directory represented by DIR -- and any suitable parents + thereof -- if we are able to do so. This is the case whenever: + + - there are no remaining open items within the directory, and + - the directory's XML close tag has been processed (so we know + there are no more children to worry about in the future), and + - either: + - we aren't fetching properties for this directory, or + - we've already finished fetching those properties. +*/ +static svn_error_t * +maybe_close_dir_chain(report_dir_t *dir) +{ + report_dir_t *cur_dir = dir; + + SVN_ERR(ensure_dir_opened(cur_dir)); + + while (cur_dir + && !cur_dir->ref_count + && cur_dir->tag_closed + && (!cur_dir->fetch_props || cur_dir->propfind_handler->done)) + { + report_dir_t *parent = cur_dir->parent_dir; + report_context_t *report_context = cur_dir->report_context; + svn_boolean_t propfind_in_done_list = FALSE; + svn_ra_serf__list_t *done_list; + + /* Make sure there are no references to this dir in the + active_dir_propfinds list. If there are, don't close the + directory -- which would delete the pool from which the + relevant active_dir_propfinds list item is allocated -- and + of course don't crawl upward to check the parents for + a closure opportunity, either. */ + done_list = report_context->active_dir_propfinds; + while (done_list) + { + if (done_list->data == cur_dir) + { + propfind_in_done_list = TRUE; + break; + } + done_list = done_list->next; + } + if (propfind_in_done_list) + break; + + SVN_ERR(close_dir(cur_dir)); + if (parent) + { + parent->ref_count--; + } + else + { + report_context->closed_root = TRUE; + } + cur_dir = parent; + } + + return SVN_NO_ERROR; +} + +/* Open the file associated with INFO for editing, pass along any + propchanges we've recorded for it, and then close the file. */ +static svn_error_t * +handle_propchange_only(report_info_t *info, + apr_pool_t *scratch_pool) +{ + SVN_ERR(open_updated_file(info, FALSE, scratch_pool)); + SVN_ERR(close_updated_file(info, scratch_pool)); + + /* We're done with our pool. */ + svn_pool_destroy(info->pool); + + info->dir->ref_count--; + + /* See if the parent directory of this file (and perhaps even + parents of that) can be closed now. */ + SVN_ERR(maybe_close_dir_chain(info->dir)); + + return SVN_NO_ERROR; +} + +/* "Fetch" a file whose contents were made available via the + get_wc_contents() callback (as opposed to requiring a GET to the + server), and feed the information through the associated update + editor. In editor-speak, this will add/open the file, transmit any + property changes, handle the contents, and then close the file. */ +static svn_error_t * +handle_local_content(report_info_t *info, + apr_pool_t *scratch_pool) +{ + SVN_ERR(svn_txdelta_send_stream(info->cached_contents, info->textdelta, + info->textdelta_baton, NULL, scratch_pool)); + SVN_ERR(svn_stream_close(info->cached_contents)); + info->cached_contents = NULL; + SVN_ERR(close_updated_file(info, scratch_pool)); + + /* We're done with our pool. */ + svn_pool_destroy(info->pool); + + info->dir->ref_count--; + + /* See if the parent directory of this fetched item (and + perhaps even parents of that) can be closed now. */ + SVN_ERR(maybe_close_dir_chain(info->dir)); + + return SVN_NO_ERROR; +} + +/* --------------------------------------------------------- */ + +static svn_error_t * +fetch_file(report_context_t *ctx, report_info_t *info) +{ + svn_ra_serf__connection_t *conn; + svn_ra_serf__handler_t *handler; + + /* What connection should we go on? */ + conn = get_best_connection(ctx); + + /* If needed, create the PROPFIND to retrieve the file's properties. */ + info->propfind_handler = NULL; + if (info->fetch_props) + { + SVN_ERR(svn_ra_serf__deliver_props(&info->propfind_handler, info->props, + ctx->sess, conn, info->url, + ctx->target_rev, "0", all_props, + &ctx->done_propfinds, + info->dir->pool)); + SVN_ERR_ASSERT(info->propfind_handler); + + /* Create a serf request for the PROPFIND. */ + svn_ra_serf__request_create(info->propfind_handler); + + ctx->num_active_propfinds++; + } + + /* If we've been asked to fetch the file or it's an add, do so. + * Otherwise, handle the case where only the properties changed. + */ + if (info->fetch_file && ctx->text_deltas) + { + svn_stream_t *contents = NULL; + + /* Open the file for editing. */ + SVN_ERR(open_updated_file(info, FALSE, info->pool)); + + if (info->textdelta == svn_delta_noop_window_handler) + { + /* There is nobody looking for an actual stream. + + Just report an empty stream instead of fetching + to be ingored data */ + info->cached_contents = svn_stream_empty(info->pool); + } + else if (ctx->sess->wc_callbacks->get_wc_contents + && info->final_sha1_checksum) + { + svn_error_t *err = NULL; + svn_checksum_t *checksum = NULL; + + /* Parse the optional SHA1 checksum (1.7+) */ + err = svn_checksum_parse_hex(&checksum, svn_checksum_sha1, + info->final_sha1_checksum, + info->pool); + + /* Okay so far? Let's try to get a stream on some readily + available matching content. */ + if (!err && checksum) + { + err = ctx->sess->wc_callbacks->get_wc_contents( + ctx->sess->wc_callback_baton, &contents, + checksum, info->pool); + + if (! err) + info->cached_contents = contents; + } + + if (err) + { + /* Meh. Maybe we'll care one day why we're in an + errorful state, but this codepath is optional. */ + svn_error_clear(err); + } + } + + /* If the working copy can provide cached contents for this + file, we don't have to fetch them from the server. */ + if (info->cached_contents) + { + /* If we'll be doing a PROPFIND for this file... */ + if (info->propfind_handler) + { + /* ... then we'll just leave ourselves a little "todo" + about that fact (and we'll deal with the file content + stuff later, after we've handled that PROPFIND + response. */ + svn_ra_serf__list_t *list_item; + + list_item = apr_pcalloc(info->dir->pool, sizeof(*list_item)); + list_item->data = info; + list_item->next = ctx->file_propchanges_only; + ctx->file_propchanges_only = list_item; + } + else + { + /* Otherwise, if we've no PROPFIND to do, we might as + well take care of those locally accessible file + contents now. */ + SVN_ERR(handle_local_content(info, info->pool)); + } + } + else + { + /* Otherwise, we use a GET request for the file's contents. */ + report_fetch_t *fetch_ctx; + + fetch_ctx = apr_pcalloc(info->dir->pool, sizeof(*fetch_ctx)); + fetch_ctx->info = info; + fetch_ctx->done_list = &ctx->done_fetches; + fetch_ctx->sess = ctx->sess; + fetch_ctx->conn = conn; + + handler = apr_pcalloc(info->dir->pool, sizeof(*handler)); + + handler->handler_pool = info->dir->pool; + handler->method = "GET"; + handler->path = fetch_ctx->info->url; + + handler->conn = conn; + handler->session = ctx->sess; + + handler->custom_accept_encoding = TRUE; + handler->header_delegate = headers_fetch; + handler->header_delegate_baton = fetch_ctx; + + handler->response_handler = handle_fetch; + handler->response_baton = fetch_ctx; + + handler->response_error = cancel_fetch; + handler->response_error_baton = fetch_ctx; + + fetch_ctx->handler = handler; + + svn_ra_serf__request_create(handler); + + ctx->num_active_fetches++; + } + } + else if (info->propfind_handler) + { + svn_ra_serf__list_t *list_item; + + list_item = apr_pcalloc(info->dir->pool, sizeof(*list_item)); + list_item->data = info; + list_item->next = ctx->file_propchanges_only; + ctx->file_propchanges_only = list_item; + } + else + { + /* No propfind or GET request. Just handle the prop changes now. */ + SVN_ERR(handle_propchange_only(info, info->pool)); + } + + if (ctx->num_active_fetches + ctx->num_active_propfinds + > REQUEST_COUNT_TO_PAUSE) + ctx->parser_ctx->paused = TRUE; + + return SVN_NO_ERROR; +} + + +/** XML callbacks for our update-report response parsing */ + +static svn_error_t * +start_report(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = parser->user_data; + report_state_e state; + + state = parser->state->current_state; + + if (state == NONE && strcmp(name.name, "update-report") == 0) + { + const char *val; + + val = svn_xml_get_attr_value("inline-props", attrs); + if (val && (strcmp(val, "true") == 0)) + ctx->add_props_included = TRUE; + + val = svn_xml_get_attr_value("send-all", attrs); + if (val && (strcmp(val, "true") == 0)) + { + ctx->send_all_mode = TRUE; + + /* All properties are included in send-all mode. */ + ctx->add_props_included = TRUE; + } + } + else if (state == NONE && strcmp(name.name, "target-revision") == 0) + { + const char *rev; + + rev = svn_xml_get_attr_value("rev", attrs); + + if (!rev) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in target-revision element")); + } + + SVN_ERR(ctx->update_editor->set_target_revision(ctx->update_baton, + SVN_STR_TO_REV(rev), + ctx->sess->pool)); + } + else if (state == NONE && strcmp(name.name, "open-directory") == 0) + { + const char *rev; + report_info_t *info; + + rev = svn_xml_get_attr_value("rev", attrs); + + if (!rev) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-directory element")); + } + + info = push_state(parser, ctx, OPEN_DIR); + + info->base_rev = SVN_STR_TO_REV(rev); + info->dir->base_rev = info->base_rev; + info->fetch_props = TRUE; + + info->dir->base_name = ""; + info->dir->name = ""; + + info->base_name = info->dir->base_name; + info->name = info->dir->name; + + info->dir->repos_relpath = svn_hash_gets(ctx->switched_paths, ""); + + if (!info->dir->repos_relpath) + SVN_ERR(svn_ra_serf__get_relative_path(&info->dir->repos_relpath, + ctx->sess->session_url.path, + ctx->sess, ctx->conn, + info->dir->pool)); + } + else if (state == NONE) + { + /* do nothing as we haven't seen our valid start tag yet. */ + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "open-directory") == 0) + { + const char *rev, *dirname; + report_dir_t *dir; + report_info_t *info; + + rev = svn_xml_get_attr_value("rev", attrs); + + if (!rev) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-directory element")); + } + + dirname = svn_xml_get_attr_value("name", attrs); + + if (!dirname) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in open-directory element")); + } + + info = push_state(parser, ctx, OPEN_DIR); + + dir = info->dir; + + info->base_rev = SVN_STR_TO_REV(rev); + dir->base_rev = info->base_rev; + + info->fetch_props = FALSE; + + dir->base_name = apr_pstrdup(dir->pool, dirname); + info->base_name = dir->base_name; + + /* Expand our name. */ + dir->name = svn_relpath_join(dir->parent_dir->name, dir->base_name, + dir->pool); + info->name = dir->name; + + dir->repos_relpath = svn_hash_gets(ctx->switched_paths, dir->name); + + if (!dir->repos_relpath) + dir->repos_relpath = svn_relpath_join(dir->parent_dir->repos_relpath, + dir->base_name, dir->pool); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "add-directory") == 0) + { + const char *dir_name, *cf, *cr; + report_dir_t *dir; + report_info_t *info; + + dir_name = svn_xml_get_attr_value("name", attrs); + if (!dir_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in add-directory element")); + } + cf = svn_xml_get_attr_value("copyfrom-path", attrs); + cr = svn_xml_get_attr_value("copyfrom-rev", attrs); + + info = push_state(parser, ctx, ADD_DIR); + + dir = info->dir; + + dir->base_name = apr_pstrdup(dir->pool, dir_name); + info->base_name = dir->base_name; + + /* Expand our name. */ + dir->name = svn_relpath_join(dir->parent_dir->name, dir->base_name, + dir->pool); + info->name = dir->name; + + info->copyfrom_path = cf ? apr_pstrdup(info->pool, cf) : NULL; + info->copyfrom_rev = cr ? SVN_STR_TO_REV(cr) : SVN_INVALID_REVNUM; + + /* Mark that we don't have a base. */ + info->base_rev = SVN_INVALID_REVNUM; + dir->base_rev = info->base_rev; + + /* If the server isn't included properties for added items, + we'll need to fetch them ourselves. */ + if (! ctx->add_props_included) + dir->fetch_props = TRUE; + + dir->repos_relpath = svn_relpath_join(dir->parent_dir->repos_relpath, + dir->base_name, dir->pool); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "open-file") == 0) + { + const char *file_name, *rev; + report_info_t *info; + + file_name = svn_xml_get_attr_value("name", attrs); + + if (!file_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in open-file element")); + } + + rev = svn_xml_get_attr_value("rev", attrs); + + if (!rev) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing revision attr in open-file element")); + } + + info = push_state(parser, ctx, OPEN_FILE); + + info->base_rev = SVN_STR_TO_REV(rev); + info->fetch_props = FALSE; + + info->base_name = apr_pstrdup(info->pool, file_name); + info->name = NULL; + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "add-file") == 0) + { + const char *file_name, *cf, *cr; + report_info_t *info; + + file_name = svn_xml_get_attr_value("name", attrs); + cf = svn_xml_get_attr_value("copyfrom-path", attrs); + cr = svn_xml_get_attr_value("copyfrom-rev", attrs); + + if (!file_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in add-file element")); + } + + info = push_state(parser, ctx, ADD_FILE); + + info->base_rev = SVN_INVALID_REVNUM; + + /* If the server isn't in "send-all" mode, we should expect to + fetch contents for added files. */ + if (! ctx->send_all_mode) + info->fetch_file = TRUE; + + /* If the server isn't included properties for added items, + we'll need to fetch them ourselves. */ + if (! ctx->add_props_included) + info->fetch_props = TRUE; + + info->base_name = apr_pstrdup(info->pool, file_name); + info->name = NULL; + + info->copyfrom_path = cf ? apr_pstrdup(info->pool, cf) : NULL; + info->copyfrom_rev = cr ? SVN_STR_TO_REV(cr) : SVN_INVALID_REVNUM; + + info->final_sha1_checksum = + svn_xml_get_attr_value("sha1-checksum", attrs); + if (info->final_sha1_checksum) + info->final_sha1_checksum = apr_pstrdup(info->pool, + info->final_sha1_checksum); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "delete-entry") == 0) + { + const char *file_name; + const char *rev_str; + report_info_t *info; + apr_pool_t *tmppool; + const char *full_path; + svn_revnum_t delete_rev = SVN_INVALID_REVNUM; + + file_name = svn_xml_get_attr_value("name", attrs); + + if (!file_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in delete-entry element")); + } + + rev_str = svn_xml_get_attr_value("rev", attrs); + if (rev_str) /* Not available on older repositories! */ + delete_rev = SVN_STR_TO_REV(rev_str); + + info = parser->state->private; + + SVN_ERR(ensure_dir_opened(info->dir)); + + tmppool = svn_pool_create(info->dir->dir_baton_pool); + + full_path = svn_relpath_join(info->dir->name, file_name, tmppool); + + SVN_ERR(ctx->update_editor->delete_entry(full_path, + delete_rev, + info->dir->dir_baton, + tmppool)); + + svn_pool_destroy(tmppool); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "absent-directory") == 0) + { + const char *file_name; + report_info_t *info; + + file_name = svn_xml_get_attr_value("name", attrs); + + if (!file_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in absent-directory element")); + } + + info = parser->state->private; + + SVN_ERR(ensure_dir_opened(info->dir)); + + SVN_ERR(ctx->update_editor->absent_directory( + svn_relpath_join(info->name, file_name, + info->dir->pool), + info->dir->dir_baton, + info->dir->pool)); + } + else if ((state == OPEN_DIR || state == ADD_DIR) && + strcmp(name.name, "absent-file") == 0) + { + const char *file_name; + report_info_t *info; + + file_name = svn_xml_get_attr_value("name", attrs); + + if (!file_name) + { + return svn_error_create( + SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in absent-file element")); + } + + info = parser->state->private; + + SVN_ERR(ensure_dir_opened(info->dir)); + + SVN_ERR(ctx->update_editor->absent_file( + svn_relpath_join(info->name, file_name, + info->dir->pool), + info->dir->dir_baton, + info->dir->pool)); + } + else if (state == OPEN_DIR || state == ADD_DIR) + { + report_info_t *info; + + if (strcmp(name.name, "checked-in") == 0) + { + info = push_state(parser, ctx, IGNORE_PROP_NAME); + info->prop_ns = name.namespace; + info->prop_name = apr_pstrdup(parser->state->pool, name.name); + info->prop_encoding = NULL; + svn_stringbuf_setempty(info->prop_value); + } + else if (strcmp(name.name, "set-prop") == 0 || + strcmp(name.name, "remove-prop") == 0) + { + const char *full_prop_name; + const char *colon; + + info = push_state(parser, ctx, PROP); + + full_prop_name = svn_xml_get_attr_value("name", attrs); + if (!full_prop_name) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in %s element"), + name.name); + } + + colon = strchr(full_prop_name, ':'); + + if (colon) + colon++; + else + colon = full_prop_name; + + info->prop_ns = apr_pstrmemdup(info->dir->pool, full_prop_name, + colon - full_prop_name); + info->prop_name = apr_pstrdup(parser->state->pool, colon); + info->prop_encoding = svn_xml_get_attr_value("encoding", attrs); + svn_stringbuf_setempty(info->prop_value); + } + else if (strcmp(name.name, "prop") == 0) + { + /* need to fetch it. */ + push_state(parser, ctx, NEED_PROP_NAME); + } + else if (strcmp(name.name, "fetch-props") == 0) + { + info = parser->state->private; + + info->dir->fetch_props = TRUE; + } + else + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unknown tag '%s' while at state %d"), + name.name, state); + } + + } + else if (state == OPEN_FILE || state == ADD_FILE) + { + report_info_t *info; + + if (strcmp(name.name, "checked-in") == 0) + { + info = push_state(parser, ctx, IGNORE_PROP_NAME); + info->prop_ns = name.namespace; + info->prop_name = apr_pstrdup(parser->state->pool, name.name); + info->prop_encoding = NULL; + svn_stringbuf_setempty(info->prop_value); + } + else if (strcmp(name.name, "prop") == 0) + { + /* need to fetch it. */ + push_state(parser, ctx, NEED_PROP_NAME); + } + else if (strcmp(name.name, "fetch-props") == 0) + { + info = parser->state->private; + + info->fetch_props = TRUE; + } + else if (strcmp(name.name, "fetch-file") == 0) + { + info = parser->state->private; + info->base_checksum = svn_xml_get_attr_value("base-checksum", attrs); + + if (info->base_checksum) + info->base_checksum = apr_pstrdup(info->pool, info->base_checksum); + + info->final_sha1_checksum = + svn_xml_get_attr_value("sha1-checksum", attrs); + if (info->final_sha1_checksum) + info->final_sha1_checksum = apr_pstrdup(info->pool, + info->final_sha1_checksum); + + info->fetch_file = TRUE; + } + else if (strcmp(name.name, "set-prop") == 0 || + strcmp(name.name, "remove-prop") == 0) + { + const char *full_prop_name; + const char *colon; + + info = push_state(parser, ctx, PROP); + + full_prop_name = svn_xml_get_attr_value("name", attrs); + if (!full_prop_name) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Missing name attr in %s element"), + name.name); + } + colon = strchr(full_prop_name, ':'); + + if (colon) + colon++; + else + colon = full_prop_name; + + info->prop_ns = apr_pstrmemdup(info->dir->pool, full_prop_name, + colon - full_prop_name); + info->prop_name = apr_pstrdup(parser->state->pool, colon); + info->prop_encoding = svn_xml_get_attr_value("encoding", attrs); + svn_stringbuf_setempty(info->prop_value); + } + else if (strcmp(name.name, "txdelta") == 0) + { + /* Pre 1.2, mod_dav_svn was using <txdelta> tags (in + addition to <fetch-file>s and such) when *not* in + "send-all" mode. As a client, we're smart enough to know + that's wrong, so we'll just ignore these tags. */ + if (ctx->send_all_mode) + { + const svn_delta_editor_t *update_editor = ctx->update_editor; + + info = push_state(parser, ctx, TXDELTA); + + if (! info->file_baton) + { + SVN_ERR(open_updated_file(info, FALSE, info->pool)); + } + + info->base_checksum = svn_xml_get_attr_value("base-checksum", + attrs); + SVN_ERR(update_editor->apply_textdelta(info->file_baton, + info->base_checksum, + info->editor_pool, + &info->textdelta, + &info->textdelta_baton)); + info->svndiff_decoder = svn_txdelta_parse_svndiff( + info->textdelta, + info->textdelta_baton, + TRUE, info->pool); + info->base64_decoder = svn_base64_decode(info->svndiff_decoder, + info->pool); + } + } + else + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unknown tag '%s' while at state %d"), + name.name, state); + } + } + else if (state == IGNORE_PROP_NAME) + { + report_info_t *info = push_state(parser, ctx, PROP); + info->prop_encoding = svn_xml_get_attr_value("encoding", attrs); + } + else if (state == NEED_PROP_NAME) + { + report_info_t *info; + + info = push_state(parser, ctx, PROP); + + info->prop_ns = name.namespace; + info->prop_name = apr_pstrdup(parser->state->pool, name.name); + info->prop_encoding = svn_xml_get_attr_value("encoding", attrs); + svn_stringbuf_setempty(info->prop_value); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +end_report(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = parser->user_data; + report_state_e state; + + state = parser->state->current_state; + + if (state == NONE) + { + if (strcmp(name.name, "update-report") == 0) + { + ctx->report_completed = TRUE; + } + else + { + /* nothing to close yet. */ + return SVN_NO_ERROR; + } + } + + if (((state == OPEN_DIR && (strcmp(name.name, "open-directory") == 0)) || + (state == ADD_DIR && (strcmp(name.name, "add-directory") == 0)))) + { + const char *checked_in_url; + report_info_t *info = parser->state->private; + + /* We've now closed this directory; note it. */ + info->dir->tag_closed = TRUE; + + /* go fetch info->file_name from DAV:checked-in */ + checked_in_url = + svn_ra_serf__get_ver_prop(info->dir->props, info->base_name, + info->base_rev, "DAV:", "checked-in"); + + /* If we were expecting to have the properties and we aren't able to + * get it, bail. + */ + if (!checked_in_url && + (!SVN_IS_VALID_REVNUM(info->dir->base_rev) || info->dir->fetch_props)) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("The REPORT or PROPFIND response did not " + "include the requested checked-in value")); + } + + info->dir->url = checked_in_url; + + /* At this point, we should have the checked-in href. + * If needed, create the PROPFIND to retrieve the dir's properties. + */ + if (info->dir->fetch_props) + { + svn_ra_serf__list_t *list_item; + + SVN_ERR(svn_ra_serf__deliver_props(&info->dir->propfind_handler, + info->dir->props, ctx->sess, + get_best_connection(ctx), + info->dir->url, + ctx->target_rev, "0", + all_props, + &ctx->done_dir_propfinds, + info->dir->pool)); + SVN_ERR_ASSERT(info->dir->propfind_handler); + + /* Create a serf request for the PROPFIND. */ + svn_ra_serf__request_create(info->dir->propfind_handler); + + ctx->num_active_propfinds++; + + list_item = apr_pcalloc(info->dir->pool, sizeof(*list_item)); + list_item->data = info->dir; + list_item->next = ctx->active_dir_propfinds; + ctx->active_dir_propfinds = list_item; + + if (ctx->num_active_fetches + ctx->num_active_propfinds + > REQUEST_COUNT_TO_PAUSE) + ctx->parser_ctx->paused = TRUE; + } + else + { + info->dir->propfind_handler = NULL; + } + + /* See if this directory (and perhaps even parents of that) can + be closed now. This is likely to be the case only if we + didn't need to contact the server for supplemental + information required to handle any of this directory's + children. */ + SVN_ERR(maybe_close_dir_chain(info->dir)); + svn_ra_serf__xml_pop_state(parser); + } + else if (state == OPEN_FILE && strcmp(name.name, "open-file") == 0) + { + report_info_t *info = parser->state->private; + + /* Expand our full name now if we haven't done so yet. */ + if (!info->name) + { + info->name = svn_relpath_join(info->dir->name, info->base_name, + info->pool); + } + + info->lock_token = svn_hash_gets(ctx->lock_path_tokens, info->name); + + if (info->lock_token && !info->fetch_props) + info->fetch_props = TRUE; + + /* If possible, we'd like to fetch only a delta against a + * version of the file we already have in our working copy, + * rather than fetching a fulltext. + * + * In HTTP v2, we can simply construct the URL we need given the + * repos_relpath and base revision number. + */ + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(ctx->sess)) + { + const char *repos_relpath; + + /* If this file is switched vs the editor root we should provide + its real url instead of the one calculated from the session root. + */ + repos_relpath = svn_hash_gets(ctx->switched_paths, info->name); + + if (!repos_relpath) + { + if (ctx->root_is_switched) + { + /* We are updating a direct target (most likely a file) + that is switched vs its parent url */ + SVN_ERR_ASSERT(*svn_relpath_dirname(info->name, info->pool) + == '\0'); + + repos_relpath = svn_hash_gets(ctx->switched_paths, ""); + } + else + repos_relpath = svn_relpath_join(info->dir->repos_relpath, + info->base_name, info->pool); + } + + info->delta_base = apr_psprintf(info->pool, "%s/%ld/%s", + ctx->sess->rev_root_stub, + info->base_rev, + svn_path_uri_encode(repos_relpath, + info->pool)); + } + else if (ctx->sess->wc_callbacks->get_wc_prop) + { + /* If we have a WC, we might be able to dive all the way into the WC + * to get the previous URL so we can do a differential GET with the + * base URL. + */ + const svn_string_t *value = NULL; + SVN_ERR(ctx->sess->wc_callbacks->get_wc_prop( + ctx->sess->wc_callback_baton, info->name, + SVN_RA_SERF__WC_CHECKED_IN_URL, &value, info->pool)); + + info->delta_base = value ? value->data : NULL; + } + + /* go fetch info->name from DAV:checked-in */ + info->url = svn_ra_serf__get_ver_prop(info->props, info->base_name, + info->base_rev, "DAV:", "checked-in"); + if (!info->url) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("The REPORT or PROPFIND response did not " + "include the requested checked-in value")); + } + + /* If the server is in "send-all" mode, we might have opened the + file when we started seeing content for it. If we didn't get + any content for it, we still need to open the file. But in + any case, we can then immediately close it. */ + if (ctx->send_all_mode) + { + if (! info->file_baton) + { + SVN_ERR(open_updated_file(info, FALSE, info->pool)); + } + SVN_ERR(close_updated_file(info, info->pool)); + info->dir->ref_count--; + } + /* Otherwise, if the server is *not* in "send-all" mode, we + should be at a point where we can queue up any auxiliary + content-fetching requests. */ + else + { + SVN_ERR(fetch_file(ctx, info)); + } + + svn_ra_serf__xml_pop_state(parser); + } + else if (state == ADD_FILE && strcmp(name.name, "add-file") == 0) + { + report_info_t *info = parser->state->private; + + /* go fetch info->name from DAV:checked-in */ + info->url = svn_ra_serf__get_ver_prop(info->props, info->base_name, + info->base_rev, "DAV:", "checked-in"); + if (!info->url) + { + return svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("The REPORT or PROPFIND response did not " + "include the requested checked-in value")); + } + + /* If the server is in "send-all" mode, we might have opened the + file when we started seeing content for it. If we didn't get + any content for it, we still need to open the file. But in + any case, we can then immediately close it. */ + if (ctx->send_all_mode) + { + if (! info->file_baton) + { + SVN_ERR(open_updated_file(info, FALSE, info->pool)); + } + SVN_ERR(close_updated_file(info, info->pool)); + info->dir->ref_count--; + } + /* Otherwise, if the server is *not* in "send-all" mode, we + should be at a point where we can queue up any auxiliary + content-fetching requests. */ + else + { + SVN_ERR(fetch_file(ctx, info)); + } + + svn_ra_serf__xml_pop_state(parser); + } + else if (state == TXDELTA && strcmp(name.name, "txdelta") == 0) + { + report_info_t *info = parser->state->private; + + /* Pre 1.2, mod_dav_svn was using <txdelta> tags (in addition to + <fetch-file>s and such) when *not* in "send-all" mode. As a + client, we're smart enough to know that's wrong, so when not + in "receiving-all" mode, we'll ignore these tags. */ + if (ctx->send_all_mode) + { + SVN_ERR(svn_stream_close(info->base64_decoder)); + } + + svn_ra_serf__xml_pop_state(parser); + } + else if (state == PROP) + { + /* We need to move the prop_ns, prop_name, and prop_value into the + * same lifetime as the dir->pool. + */ + svn_ra_serf__ns_t *ns, *ns_name_match; + svn_boolean_t found = FALSE; + report_info_t *info; + report_dir_t *dir; + apr_hash_t *props; + const svn_string_t *set_val_str; + apr_pool_t *pool; + + info = parser->state->private; + dir = info->dir; + + /* We're going to be slightly tricky. We don't care what the ->url + * field is here at this point. So, we're going to stick a single + * copy of the property name inside of the ->url field. + */ + ns_name_match = NULL; + for (ns = dir->ns_list; ns; ns = ns->next) + { + if (strcmp(ns->namespace, info->prop_ns) == 0) + { + ns_name_match = ns; + if (strcmp(ns->url, info->prop_name) == 0) + { + found = TRUE; + break; + } + } + } + + if (!found) + { + ns = apr_palloc(dir->pool, sizeof(*ns)); + if (!ns_name_match) + { + ns->namespace = apr_pstrdup(dir->pool, info->prop_ns); + } + else + { + ns->namespace = ns_name_match->namespace; + } + ns->url = apr_pstrdup(dir->pool, info->prop_name); + + ns->next = dir->ns_list; + dir->ns_list = ns; + } + + if (strcmp(name.name, "remove-prop") != 0) + { + props = info->props; + pool = info->pool; + } + else + { + props = dir->removed_props; + pool = dir->pool; + svn_stringbuf_setempty(info->prop_value); + } + + if (info->prop_encoding) + { + if (strcmp(info->prop_encoding, "base64") == 0) + { + svn_string_t tmp; + + /* Don't use morph_info_string cuz we need prop_value to + remain usable. */ + tmp.data = info->prop_value->data; + tmp.len = info->prop_value->len; + + set_val_str = svn_base64_decode_string(&tmp, pool); + } + else + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, + NULL, + _("Got unrecognized encoding '%s'"), + info->prop_encoding); + } + } + else + { + set_val_str = svn_string_create_from_buf(info->prop_value, pool); + } + + svn_ra_serf__set_ver_prop(props, info->base_name, info->base_rev, + ns->namespace, ns->url, set_val_str, pool); + + /* Advance handling: if we spotted the md5-checksum property on + the wire, remember it's value. */ + if (strcmp(ns->url, "md5-checksum") == 0 + && strcmp(ns->namespace, SVN_DAV_PROP_NS_DAV) == 0) + info->final_checksum = apr_pstrdup(info->pool, set_val_str->data); + + svn_ra_serf__xml_pop_state(parser); + } + else if (state == IGNORE_PROP_NAME || state == NEED_PROP_NAME) + { + svn_ra_serf__xml_pop_state(parser); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +cdata_report(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + report_context_t *ctx = parser->user_data; + + UNUSED_CTX(ctx); + + if (parser->state->current_state == PROP) + { + report_info_t *info = parser->state->private; + + svn_stringbuf_appendbytes(info->prop_value, data, len); + } + else if (parser->state->current_state == TXDELTA) + { + /* Pre 1.2, mod_dav_svn was using <txdelta> tags (in addition to + <fetch-file>s and such) when *not* in "send-all" mode. As a + client, we're smart enough to know that's wrong, so when not + in "receiving-all" mode, we'll ignore these tags. */ + if (ctx->send_all_mode) + { + apr_size_t nlen = len; + report_info_t *info = parser->state->private; + + SVN_ERR(svn_stream_write(info->base64_decoder, data, &nlen)); + if (nlen != len) + { + /* Short write without associated error? "Can't happen." */ + return svn_error_createf(SVN_ERR_STREAM_UNEXPECTED_EOF, NULL, + _("Error writing to '%s': unexpected EOF"), + info->name); + } + } + } + + return SVN_NO_ERROR; +} + + +/** Editor callbacks given to callers to create request body */ + +/* Helper to create simple xml tag without attributes. */ +static void +make_simple_xml_tag(svn_stringbuf_t **buf_p, + const char *tagname, + const char *cdata, + apr_pool_t *pool) +{ + svn_xml_make_open_tag(buf_p, pool, svn_xml_protect_pcdata, tagname, NULL); + svn_xml_escape_cdata_cstring(buf_p, cdata, pool); + svn_xml_make_close_tag(buf_p, pool, tagname); +} + +static svn_error_t * +set_path(void *report_baton, + const char *path, + svn_revnum_t revision, + svn_depth_t depth, + svn_boolean_t start_empty, + const char *lock_token, + apr_pool_t *pool) +{ + report_context_t *report = report_baton; + svn_stringbuf_t *buf = NULL; + + svn_xml_make_open_tag(&buf, pool, svn_xml_protect_pcdata, "S:entry", + "rev", apr_ltoa(pool, revision), + "lock-token", lock_token, + "depth", svn_depth_to_word(depth), + "start-empty", start_empty ? "true" : NULL, + NULL); + svn_xml_escape_cdata_cstring(&buf, path, pool); + svn_xml_make_close_tag(&buf, pool, "S:entry"); + + SVN_ERR(svn_io_file_write_full(report->body_file, buf->data, buf->len, + NULL, pool)); + + if (lock_token) + { + svn_hash_sets(report->lock_path_tokens, + apr_pstrdup(report->pool, path), + apr_pstrdup(report->pool, lock_token)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +delete_path(void *report_baton, + const char *path, + apr_pool_t *pool) +{ + report_context_t *report = report_baton; + svn_stringbuf_t *buf = NULL; + + make_simple_xml_tag(&buf, "S:missing", path, pool); + + SVN_ERR(svn_io_file_write_full(report->body_file, buf->data, buf->len, + NULL, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +link_path(void *report_baton, + const char *path, + const char *url, + svn_revnum_t revision, + svn_depth_t depth, + svn_boolean_t start_empty, + const char *lock_token, + apr_pool_t *pool) +{ + report_context_t *report = report_baton; + const char *link, *report_target; + apr_uri_t uri; + apr_status_t status; + svn_stringbuf_t *buf = NULL; + + /* We need to pass in the baseline relative path. + * + * TODO Confirm that it's on the same server? + */ + status = apr_uri_parse(pool, url, &uri); + if (status) + { + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Unable to parse URL '%s'"), url); + } + + SVN_ERR(svn_ra_serf__report_resource(&report_target, report->sess, + NULL, pool)); + SVN_ERR(svn_ra_serf__get_relative_path(&link, uri.path, report->sess, + NULL, pool)); + + link = apr_pstrcat(pool, "/", link, (char *)NULL); + + svn_xml_make_open_tag(&buf, pool, svn_xml_protect_pcdata, "S:entry", + "rev", apr_ltoa(pool, revision), + "lock-token", lock_token, + "depth", svn_depth_to_word(depth), + "linkpath", link, + "start-empty", start_empty ? "true" : NULL, + NULL); + svn_xml_escape_cdata_cstring(&buf, path, pool); + svn_xml_make_close_tag(&buf, pool, "S:entry"); + + SVN_ERR(svn_io_file_write_full(report->body_file, buf->data, buf->len, + NULL, pool)); + + /* Store the switch roots to allow generating repos_relpaths from just + the working copy paths. (Needed for HTTPv2) */ + path = apr_pstrdup(report->pool, path); + svn_hash_sets(report->switched_paths, + path, apr_pstrdup(report->pool, link + 1)); + + if (!*path) + report->root_is_switched = TRUE; + + if (lock_token) + { + svn_hash_sets(report->lock_path_tokens, + path, apr_pstrdup(report->pool, lock_token)); + } + + return APR_SUCCESS; +} + +/** Minimum nr. of outstanding requests needed before a new connection is + * opened. */ +#define REQS_PER_CONN 8 + +/** This function creates a new connection for this serf session, but only + * if the number of NUM_ACTIVE_REQS > REQS_PER_CONN or if there currently is + * only one main connection open. + */ +static svn_error_t * +open_connection_if_needed(svn_ra_serf__session_t *sess, int num_active_reqs) +{ + /* For each REQS_PER_CONN outstanding requests open a new connection, with + * a minimum of 1 extra connection. */ + if (sess->num_conns == 1 || + ((num_active_reqs / REQS_PER_CONN) > sess->num_conns)) + { + int cur = sess->num_conns; + apr_status_t status; + + sess->conns[cur] = apr_pcalloc(sess->pool, sizeof(*sess->conns[cur])); + sess->conns[cur]->bkt_alloc = serf_bucket_allocator_create(sess->pool, + NULL, NULL); + sess->conns[cur]->last_status_code = -1; + sess->conns[cur]->session = sess; + status = serf_connection_create2(&sess->conns[cur]->conn, + sess->context, + sess->session_url, + svn_ra_serf__conn_setup, + sess->conns[cur], + svn_ra_serf__conn_closed, + sess->conns[cur], + sess->pool); + if (status) + return svn_ra_serf__wrap_err(status, NULL); + + sess->num_conns++; + } + + return SVN_NO_ERROR; +} + +/* Serf callback to create update request body bucket. */ +static svn_error_t * +create_update_report_body(serf_bucket_t **body_bkt, + void *baton, + serf_bucket_alloc_t *alloc, + apr_pool_t *pool) +{ + report_context_t *report = baton; + apr_off_t offset; + + offset = 0; + apr_file_seek(report->body_file, APR_SET, &offset); + + *body_bkt = serf_bucket_file_create(report->body_file, alloc); + + return SVN_NO_ERROR; +} + +/* Serf callback to setup update request headers. */ +static svn_error_t * +setup_update_report_headers(serf_bucket_t *headers, + void *baton, + apr_pool_t *pool) +{ + report_context_t *report = baton; + + if (report->sess->using_compression) + { + serf_bucket_headers_setn(headers, "Accept-Encoding", + "gzip;svndiff1;q=0.9,svndiff;q=0.8"); + } + else + { + serf_bucket_headers_setn(headers, "Accept-Encoding", + "svndiff1;q=0.9,svndiff;q=0.8"); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +finish_report(void *report_baton, + apr_pool_t *pool) +{ + report_context_t *report = report_baton; + svn_ra_serf__session_t *sess = report->sess; + svn_ra_serf__handler_t *handler; + svn_ra_serf__xml_parser_t *parser_ctx; + const char *report_target; + svn_stringbuf_t *buf = NULL; + apr_pool_t *iterpool = svn_pool_create(pool); + svn_error_t *err; + apr_interval_time_t waittime_left = sess->timeout; + + svn_xml_make_close_tag(&buf, iterpool, "S:update-report"); + SVN_ERR(svn_io_file_write_full(report->body_file, buf->data, buf->len, + NULL, iterpool)); + + /* We need to flush the file, make it unbuffered (so that it can be + * zero-copied via mmap), and reset the position before attempting to + * deliver the file. + * + * N.B. If we have APR 1.3+, we can unbuffer the file to let us use mmap + * and zero-copy the PUT body. However, on older APR versions, we can't + * check the buffer status; but serf will fall through and create a file + * bucket for us on the buffered svndiff handle. + */ + apr_file_flush(report->body_file); +#if APR_VERSION_AT_LEAST(1, 3, 0) + apr_file_buffer_set(report->body_file, NULL, 0); +#endif + + SVN_ERR(svn_ra_serf__report_resource(&report_target, sess, NULL, pool)); + + /* create and deliver request */ + report->path = report_target; + + handler = apr_pcalloc(pool, sizeof(*handler)); + + handler->handler_pool = pool; + handler->method = "REPORT"; + handler->path = report->path; + handler->body_delegate = create_update_report_body; + handler->body_delegate_baton = report; + handler->body_type = "text/xml"; + handler->custom_accept_encoding = TRUE; + handler->header_delegate = setup_update_report_headers; + handler->header_delegate_baton = report; + handler->conn = sess->conns[0]; + handler->session = sess; + + parser_ctx = apr_pcalloc(pool, sizeof(*parser_ctx)); + + parser_ctx->pool = pool; + parser_ctx->response_type = "update-report"; + parser_ctx->user_data = report; + parser_ctx->start = start_report; + parser_ctx->end = end_report; + parser_ctx->cdata = cdata_report; + parser_ctx->done = &report->done; + + handler->response_handler = svn_ra_serf__handle_xml_parser; + handler->response_baton = parser_ctx; + + report->parser_ctx = parser_ctx; + + svn_ra_serf__request_create(handler); + + /* Open the first extra connection. */ + SVN_ERR(open_connection_if_needed(sess, 0)); + + sess->cur_conn = 1; + + /* Note that we may have no active GET or PROPFIND requests, yet the + processing has not been completed. This could be from a delay on the + network or because we've spooled the entire response into our "pending" + content of the XML parser. The DONE flag will get set when all the + XML content has been received *and* parsed. */ + while (!report->done + || report->num_active_fetches + || report->num_active_propfinds) + { + apr_pool_t *iterpool_inner; + svn_ra_serf__list_t *done_list; + int i; + apr_status_t status; + + /* Note: this throws out the old ITERPOOL_INNER. */ + svn_pool_clear(iterpool); + + if (sess->cancel_func) + SVN_ERR(sess->cancel_func(sess->cancel_baton)); + + /* We need to be careful between the outer and inner ITERPOOLs, + and what items are allocated within. */ + iterpool_inner = svn_pool_create(iterpool); + + status = serf_context_run(sess->context, + SVN_RA_SERF__CONTEXT_RUN_DURATION, + iterpool_inner); + + err = sess->pending_error; + sess->pending_error = SVN_NO_ERROR; + + if (!err && handler->done && handler->server_error) + { + err = handler->server_error->error; + } + + /* If the context duration timeout is up, we'll subtract that + duration from the total time alloted for such things. If + there's no time left, we fail with a message indicating that + the connection timed out. */ + if (APR_STATUS_IS_TIMEUP(status)) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + status = 0; + + if (sess->timeout) + { + if (waittime_left > SVN_RA_SERF__CONTEXT_RUN_DURATION) + { + waittime_left -= SVN_RA_SERF__CONTEXT_RUN_DURATION; + } + else + { + return svn_error_create(SVN_ERR_RA_DAV_CONN_TIMEOUT, NULL, + _("Connection timed out")); + } + } + } + else + { + waittime_left = sess->timeout; + } + + if (status && handler->sline.code != 200) + { + return svn_error_trace( + svn_error_compose_create( + svn_ra_serf__error_on_status(handler->sline.code, + handler->path, + handler->location), + err)); + } + SVN_ERR(err); + if (status) + { + return svn_ra_serf__wrap_err(status, _("Error retrieving REPORT")); + } + + /* Open extra connections if we have enough requests to send. */ + if (sess->num_conns < sess->max_connections) + SVN_ERR(open_connection_if_needed(sess, report->num_active_fetches + + report->num_active_propfinds)); + + /* Prune completed file PROPFINDs. */ + done_list = report->done_propfinds; + while (done_list) + { + svn_ra_serf__list_t *next_done = done_list->next; + + svn_pool_clear(iterpool_inner); + + report->num_active_propfinds--; + + /* If we have some files that we won't be fetching the content + * for, ensure that we update the file with any altered props. + */ + if (report->file_propchanges_only) + { + svn_ra_serf__list_t *cur, *prev; + + prev = NULL; + cur = report->file_propchanges_only; + + while (cur) + { + report_info_t *item = cur->data; + + if (item->propfind_handler == done_list->data) + { + break; + } + + prev = cur; + cur = cur->next; + } + + /* If we found a match, set the new props and remove this + * propchange from our list. + */ + if (cur) + { + report_info_t *info = cur->data; + + if (!prev) + { + report->file_propchanges_only = cur->next; + } + else + { + prev->next = cur->next; + } + + /* If we've got cached file content for this file, + take care of the locally collected properties and + file content at once. Otherwise, just deal with + the collected properties. + + NOTE: These functions below could delete + info->dir->pool (via maybe_close_dir_chain()), + from which is allocated the list item in + report->file_propchanges_only. + */ + if (info->cached_contents) + { + SVN_ERR(handle_local_content(info, iterpool_inner)); + } + else + { + SVN_ERR(handle_propchange_only(info, iterpool_inner)); + } + } + } + + done_list = next_done; + } + report->done_propfinds = NULL; + + /* Prune completed fetches from our list. */ + done_list = report->done_fetches; + while (done_list) + { + report_fetch_t *done_fetch = done_list->data; + svn_ra_serf__list_t *next_done = done_list->next; + report_dir_t *cur_dir; + + /* Decrease the refcount in the parent directory of the file + whose fetch has completed. */ + cur_dir = done_fetch->info->dir; + cur_dir->ref_count--; + + /* Decrement our active fetch count. */ + report->num_active_fetches--; + + /* See if the parent directory of this fetched item (and + perhaps even parents of that) can be closed now. + + NOTE: This could delete cur_dir->pool, from which is + allocated the list item in report->done_fetches. + */ + SVN_ERR(maybe_close_dir_chain(cur_dir)); + + done_list = next_done; + } + report->done_fetches = NULL; + + /* Prune completed directory PROPFINDs. */ + done_list = report->done_dir_propfinds; + while (done_list) + { + svn_ra_serf__list_t *next_done = done_list->next; + + report->num_active_propfinds--; + + if (report->active_dir_propfinds) + { + svn_ra_serf__list_t *cur, *prev; + + prev = NULL; + cur = report->active_dir_propfinds; + + while (cur) + { + report_dir_t *item = cur->data; + + if (item->propfind_handler == done_list->data) + { + break; + } + + prev = cur; + cur = cur->next; + } + SVN_ERR_ASSERT(cur); /* we expect to find a matching propfind! */ + + /* If we found a match, set the new props and remove this + * propchange from our list. + */ + if (cur) + { + report_dir_t *cur_dir = cur->data; + + if (!prev) + { + report->active_dir_propfinds = cur->next; + } + else + { + prev->next = cur->next; + } + + /* See if this directory (and perhaps even parents of that) + can be closed now. + + NOTE: This could delete cur_dir->pool, from which is + allocated the list item in report->active_dir_propfinds. + */ + SVN_ERR(maybe_close_dir_chain(cur_dir)); + } + } + + done_list = next_done; + } + report->done_dir_propfinds = NULL; + + /* If the parser is paused, and the number of active requests has + dropped far enough, then resume parsing. */ + if (parser_ctx->paused + && (report->num_active_fetches + report->num_active_propfinds + < REQUEST_COUNT_TO_RESUME)) + parser_ctx->paused = FALSE; + + /* If we have not paused the parser and it looks like data MAY be + present (we can't know for sure because of the private structure), + then go process the pending content. */ + if (!parser_ctx->paused && parser_ctx->pending != NULL) + SVN_ERR(svn_ra_serf__process_pending(parser_ctx, + &report->report_received, + iterpool_inner)); + + /* Debugging purposes only! */ + for (i = 0; i < sess->num_conns; i++) + { + serf_debug__closed_conn(sess->conns[i]->bkt_alloc); + } + } + + /* If we got a complete report, close the edit. Otherwise, abort it. */ + if (report->report_completed) + { + /* Ensure that we opened and closed our root dir and that we closed + * all of our children. */ + if (!report->closed_root && report->root_dir != NULL) + { + SVN_ERR(close_all_dirs(report->root_dir)); + } + + err = report->update_editor->close_edit(report->update_baton, iterpool); + } + else + { + /* Tell the editor that something failed */ + err = report->update_editor->abort_edit(report->update_baton, iterpool); + + err = svn_error_create(SVN_ERR_RA_DAV_MALFORMED_DATA, err, + _("Missing update-report close tag")); + } + + svn_pool_destroy(iterpool); + return svn_error_trace(err); +} + + +static svn_error_t * +abort_report(void *report_baton, + apr_pool_t *pool) +{ +#if 0 + report_context_t *report = report_baton; +#endif + + /* Should we perform some cleanup here? */ + + return SVN_NO_ERROR; +} + +static const svn_ra_reporter3_t ra_serf_reporter = { + set_path, + delete_path, + link_path, + finish_report, + abort_report +}; + + +/** RA function implementations and body */ + +static svn_error_t * +make_update_reporter(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision, + const char *src_path, + const char *dest_path, + const char *update_target, + svn_depth_t depth, + svn_boolean_t ignore_ancestry, + svn_boolean_t text_deltas, + svn_boolean_t send_copyfrom_args, + const svn_delta_editor_t *update_editor, + void *update_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + report_context_t *report; + const svn_delta_editor_t *filter_editor; + void *filter_baton; + svn_boolean_t has_target = *update_target != '\0'; + svn_boolean_t server_supports_depth; + svn_ra_serf__session_t *sess = ra_session->priv; + svn_stringbuf_t *buf = NULL; + svn_boolean_t use_bulk_updates; + + SVN_ERR(svn_ra_serf__has_capability(ra_session, &server_supports_depth, + SVN_RA_CAPABILITY_DEPTH, scratch_pool)); + /* We can skip the depth filtering when the user requested + depth_files or depth_infinity because the server will + transmit the right stuff anyway. */ + if ((depth != svn_depth_files) + && (depth != svn_depth_infinity) + && ! server_supports_depth) + { + SVN_ERR(svn_delta_depth_filter_editor(&filter_editor, + &filter_baton, + update_editor, + update_baton, + depth, has_target, + sess->pool)); + update_editor = filter_editor; + update_baton = filter_baton; + } + + report = apr_pcalloc(result_pool, sizeof(*report)); + report->pool = result_pool; + report->sess = sess; + report->conn = report->sess->conns[0]; + report->target_rev = revision; + report->ignore_ancestry = ignore_ancestry; + report->send_copyfrom_args = send_copyfrom_args; + report->text_deltas = text_deltas; + report->lock_path_tokens = apr_hash_make(report->pool); + report->switched_paths = apr_hash_make(report->pool); + + report->source = src_path; + report->destination = dest_path; + report->update_target = update_target; + + report->update_editor = update_editor; + report->update_baton = update_baton; + report->done = FALSE; + + *reporter = &ra_serf_reporter; + *report_baton = report; + + SVN_ERR(svn_io_open_unique_file3(&report->body_file, NULL, NULL, + svn_io_file_del_on_pool_cleanup, + report->pool, scratch_pool)); + + if (sess->bulk_updates == svn_tristate_true) + { + /* User would like to use bulk updates. */ + use_bulk_updates = TRUE; + } + else if (sess->bulk_updates == svn_tristate_false) + { + /* User doesn't want bulk updates. */ + use_bulk_updates = FALSE; + } + else + { + /* User doesn't have any preferences on bulk updates. Decide on server + preferences and capabilities. */ + if (sess->server_allows_bulk) + { + if (apr_strnatcasecmp(sess->server_allows_bulk, "off") == 0) + { + /* Server doesn't want bulk updates */ + use_bulk_updates = FALSE; + } + else if (apr_strnatcasecmp(sess->server_allows_bulk, "prefer") == 0) + { + /* Server prefers bulk updates, and we respect that */ + use_bulk_updates = TRUE; + } + else + { + /* Server allows bulk updates, but doesn't dictate its use. Do + whatever is the default. */ + use_bulk_updates = FALSE; + } + } + else + { + /* Pre-1.8 server didn't send the bulk_updates header. Check if server + supports inlining properties in update editor report. */ + if (sess->supports_inline_props) + { + /* Inline props supported: do not use bulk updates. */ + use_bulk_updates = FALSE; + } + else + { + /* Inline props are not supported: use bulk updates to avoid + * PROPFINDs for every added node. */ + use_bulk_updates = TRUE; + } + } + } + + if (use_bulk_updates) + { + svn_xml_make_open_tag(&buf, scratch_pool, svn_xml_normal, + "S:update-report", + "xmlns:S", SVN_XML_NAMESPACE, "send-all", "true", + NULL); + } + else + { + svn_xml_make_open_tag(&buf, scratch_pool, svn_xml_normal, + "S:update-report", + "xmlns:S", SVN_XML_NAMESPACE, + NULL); + /* Subversion 1.8+ servers can be told to send properties for newly + added items inline even when doing a skelta response. */ + make_simple_xml_tag(&buf, "S:include-props", "yes", scratch_pool); + } + + make_simple_xml_tag(&buf, "S:src-path", report->source, scratch_pool); + + if (SVN_IS_VALID_REVNUM(report->target_rev)) + { + make_simple_xml_tag(&buf, "S:target-revision", + apr_ltoa(scratch_pool, report->target_rev), + scratch_pool); + } + + if (report->destination && *report->destination) + { + make_simple_xml_tag(&buf, "S:dst-path", report->destination, + scratch_pool); + } + + if (report->update_target && *report->update_target) + { + make_simple_xml_tag(&buf, "S:update-target", report->update_target, + scratch_pool); + } + + if (report->ignore_ancestry) + { + make_simple_xml_tag(&buf, "S:ignore-ancestry", "yes", scratch_pool); + } + + if (report->send_copyfrom_args) + { + make_simple_xml_tag(&buf, "S:send-copyfrom-args", "yes", scratch_pool); + } + + /* Old servers know "recursive" but not "depth"; help them DTRT. */ + if (depth == svn_depth_files || depth == svn_depth_empty) + { + make_simple_xml_tag(&buf, "S:recursive", "no", scratch_pool); + } + + /* When in 'send-all' mode, mod_dav_svn will assume that it should + calculate and transmit real text-deltas (instead of empty windows + that merely indicate "text is changed") unless it finds this + element. + + NOTE: Do NOT count on servers actually obeying this, as some exist + which obey send-all, but do not check for this directive at all! + + NOTE 2: When not in 'send-all' mode, mod_dav_svn can still be configured to + override our request and send text-deltas. */ + if (! text_deltas) + { + make_simple_xml_tag(&buf, "S:text-deltas", "no", scratch_pool); + } + + make_simple_xml_tag(&buf, "S:depth", svn_depth_to_word(depth), scratch_pool); + + SVN_ERR(svn_io_file_write_full(report->body_file, buf->data, buf->len, + NULL, scratch_pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__do_update(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision_to_update_to, + const char *update_target, + svn_depth_t depth, + svn_boolean_t send_copyfrom_args, + svn_boolean_t ignore_ancestry, + const svn_delta_editor_t *update_editor, + void *update_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + SVN_ERR(make_update_reporter(ra_session, reporter, report_baton, + revision_to_update_to, + session->session_url.path, NULL, update_target, + depth, ignore_ancestry, TRUE /* text_deltas */, + send_copyfrom_args, + update_editor, update_baton, + result_pool, scratch_pool)); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__do_diff(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision, + const char *diff_target, + svn_depth_t depth, + svn_boolean_t ignore_ancestry, + svn_boolean_t text_deltas, + const char *versus_url, + const svn_delta_editor_t *diff_editor, + void *diff_baton, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_pool_t *scratch_pool = svn_pool_create(pool); + + SVN_ERR(make_update_reporter(ra_session, reporter, report_baton, + revision, + session->session_url.path, versus_url, diff_target, + depth, ignore_ancestry, text_deltas, FALSE, + diff_editor, diff_baton, + pool, scratch_pool)); + svn_pool_destroy(scratch_pool); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__do_status(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + const char *status_target, + svn_revnum_t revision, + svn_depth_t depth, + const svn_delta_editor_t *status_editor, + void *status_baton, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + apr_pool_t *scratch_pool = svn_pool_create(pool); + + SVN_ERR(make_update_reporter(ra_session, reporter, report_baton, + revision, + session->session_url.path, NULL, status_target, + depth, FALSE, FALSE, FALSE, + status_editor, status_baton, + pool, scratch_pool)); + svn_pool_destroy(scratch_pool); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__do_switch(svn_ra_session_t *ra_session, + const svn_ra_reporter3_t **reporter, + void **report_baton, + svn_revnum_t revision_to_switch_to, + const char *switch_target, + svn_depth_t depth, + const char *switch_url, + svn_boolean_t send_copyfrom_args, + svn_boolean_t ignore_ancestry, + const svn_delta_editor_t *switch_editor, + void *switch_baton, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + return make_update_reporter(ra_session, reporter, report_baton, + revision_to_switch_to, + session->session_url.path, + switch_url, switch_target, + depth, + ignore_ancestry, + TRUE /* text_deltas */, + send_copyfrom_args, + switch_editor, switch_baton, + result_pool, scratch_pool); +} + +/* Helper svn_ra_serf__get_file(). Attempts to fetch file contents + * using SESSION->wc_callbacks->get_wc_contents() if sha1 property is + * present in PROPS. + * + * Sets *FOUND_P to TRUE if file contents was successfuly fetched. + * + * Performs all temporary allocations in POOL. + */ +static svn_error_t * +try_get_wc_contents(svn_boolean_t *found_p, + svn_ra_serf__session_t *session, + apr_hash_t *props, + svn_stream_t *dst_stream, + apr_pool_t *pool) +{ + apr_hash_t *svn_props; + const char *sha1_checksum_prop; + svn_checksum_t *checksum; + svn_stream_t *wc_stream; + svn_error_t *err; + + /* No contents found by default. */ + *found_p = FALSE; + + if (!session->wc_callbacks->get_wc_contents) + { + /* No callback, nothing to do. */ + return SVN_NO_ERROR; + } + + + svn_props = svn_hash_gets(props, SVN_DAV_PROP_NS_DAV); + if (!svn_props) + { + /* No properties -- therefore no checksum property -- in response. */ + return SVN_NO_ERROR; + } + + sha1_checksum_prop = svn_prop_get_value(svn_props, "sha1-checksum"); + if (sha1_checksum_prop == NULL) + { + /* No checksum property in response. */ + return SVN_NO_ERROR; + } + + SVN_ERR(svn_checksum_parse_hex(&checksum, svn_checksum_sha1, + sha1_checksum_prop, pool)); + + err = session->wc_callbacks->get_wc_contents( + session->wc_callback_baton, &wc_stream, checksum, pool); + + if (err) + { + svn_error_clear(err); + + /* Ignore errors for now. */ + return SVN_NO_ERROR; + } + + if (wc_stream) + { + SVN_ERR(svn_stream_copy3(wc_stream, + svn_stream_disown(dst_stream, pool), + NULL, NULL, pool)); + *found_p = TRUE; + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_file(svn_ra_session_t *ra_session, + const char *path, + svn_revnum_t revision, + svn_stream_t *stream, + svn_revnum_t *fetched_rev, + apr_hash_t **props, + apr_pool_t *pool) +{ + svn_ra_serf__session_t *session = ra_session->priv; + svn_ra_serf__connection_t *conn; + const char *fetch_url; + apr_hash_t *fetch_props; + svn_node_kind_t res_kind; + const svn_ra_serf__dav_props_t *which_props; + + /* What connection should we go on? */ + conn = session->conns[session->cur_conn]; + + /* Fetch properties. */ + + fetch_url = svn_path_url_add_component2(session->session_url.path, path, pool); + + /* The simple case is if we want HEAD - then a GET on the fetch_url is fine. + * + * Otherwise, we need to get the baseline version for this particular + * revision and then fetch that file. + */ + if (SVN_IS_VALID_REVNUM(revision) || fetched_rev) + { + SVN_ERR(svn_ra_serf__get_stable_url(&fetch_url, fetched_rev, + session, conn, + fetch_url, revision, + pool, pool)); + revision = SVN_INVALID_REVNUM; + } + /* REVISION is always SVN_INVALID_REVNUM */ + SVN_ERR_ASSERT(!SVN_IS_VALID_REVNUM(revision)); + + if (props) + { + which_props = all_props; + } + else if (stream && session->wc_callbacks->get_wc_contents) + { + which_props = type_and_checksum_props; + } + else + { + which_props = check_path_props; + } + + SVN_ERR(svn_ra_serf__fetch_node_props(&fetch_props, conn, fetch_url, + SVN_INVALID_REVNUM, + which_props, + pool, pool)); + + /* Verify that resource type is not collection. */ + SVN_ERR(svn_ra_serf__get_resource_type(&res_kind, fetch_props)); + if (res_kind != svn_node_file) + { + return svn_error_create(SVN_ERR_FS_NOT_FILE, NULL, + _("Can't get text contents of a directory")); + } + + /* TODO Filter out all of our props into a usable format. */ + if (props) + { + /* ### flatten_props() does not copy PROPVALUE, but fetch_node_props() + ### put them into POOL, so we're okay. */ + SVN_ERR(svn_ra_serf__flatten_props(props, fetch_props, + pool, pool)); + } + + if (stream) + { + svn_boolean_t found; + SVN_ERR(try_get_wc_contents(&found, session, fetch_props, stream, pool)); + + /* No contents found in the WC, let's fetch from server. */ + if (!found) + { + report_fetch_t *stream_ctx; + svn_ra_serf__handler_t *handler; + + /* Create the fetch context. */ + stream_ctx = apr_pcalloc(pool, sizeof(*stream_ctx)); + stream_ctx->target_stream = stream; + stream_ctx->sess = session; + stream_ctx->conn = conn; + stream_ctx->info = apr_pcalloc(pool, sizeof(*stream_ctx->info)); + stream_ctx->info->name = fetch_url; + + handler = apr_pcalloc(pool, sizeof(*handler)); + + handler->handler_pool = pool; + handler->method = "GET"; + handler->path = fetch_url; + handler->conn = conn; + handler->session = session; + + handler->custom_accept_encoding = TRUE; + handler->header_delegate = headers_fetch; + handler->header_delegate_baton = stream_ctx; + + handler->response_handler = handle_stream; + handler->response_baton = stream_ctx; + + handler->response_error = cancel_fetch; + handler->response_error_baton = stream_ctx; + + stream_ctx->handler = handler; + + svn_ra_serf__request_create(handler); + + SVN_ERR(svn_ra_serf__context_run_wait(&stream_ctx->done, session, pool)); + } + } + + return SVN_NO_ERROR; +} diff --git a/subversion/libsvn_ra_serf/util.c b/subversion/libsvn_ra_serf/util.c new file mode 100644 index 0000000..c7a1716 --- /dev/null +++ b/subversion/libsvn_ra_serf/util.c @@ -0,0 +1,2614 @@ +/* + * util.c : serf utility routines for ra_serf + * + * ==================================================================== + * 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 <assert.h> + +#define APR_WANT_STRFUNC +#include <apr.h> +#include <apr_want.h> +#include <apr_fnmatch.h> + +#include <serf.h> +#include <serf_bucket_types.h> + +#include <expat.h> + +#include "svn_hash.h" +#include "svn_dirent_uri.h" +#include "svn_path.h" +#include "svn_private_config.h" +#include "svn_string.h" +#include "svn_xml.h" +#include "svn_props.h" +#include "svn_dirent_uri.h" + +#include "../libsvn_ra/ra_loader.h" +#include "private/svn_dep_compat.h" +#include "private/svn_fspath.h" +#include "private/svn_subr_private.h" + +#include "ra_serf.h" + + +/* Fix for older expat 1.95.x's that do not define + * XML_STATUS_OK/XML_STATUS_ERROR + */ +#ifndef XML_STATUS_OK +#define XML_STATUS_OK 1 +#define XML_STATUS_ERROR 0 +#endif + +#ifndef XML_VERSION_AT_LEAST +#define XML_VERSION_AT_LEAST(major,minor,patch) \ +(((major) < XML_MAJOR_VERSION) \ + || ((major) == XML_MAJOR_VERSION && (minor) < XML_MINOR_VERSION) \ + || ((major) == XML_MAJOR_VERSION && (minor) == XML_MINOR_VERSION && \ + (patch) <= XML_MICRO_VERSION)) +#endif /* APR_VERSION_AT_LEAST */ + +#if XML_VERSION_AT_LEAST(1, 95, 8) +#define EXPAT_HAS_STOPPARSER +#endif + +/* Read/write chunks of this size into the spillbuf. */ +#define PARSE_CHUNK_SIZE 8000 + +/* We will store one megabyte in memory, before switching to store content + into a temporary file. */ +#define SPILL_SIZE 1000000 + + +/* This structure records pending data for the parser in memory blocks, + and possibly into a temporary file if "too much" content arrives. */ +struct svn_ra_serf__pending_t { + /* The spillbuf where we record the pending data. */ + svn_spillbuf_t *buf; + + /* This flag is set when the network has reached EOF. The PENDING + processing can then properly detect when parsing has completed. */ + svn_boolean_t network_eof; +}; + +#define HAS_PENDING_DATA(p) ((p) != NULL && (p)->buf != NULL \ + && svn_spillbuf__get_size((p)->buf) != 0) + + +struct expat_ctx_t { + svn_ra_serf__xml_context_t *xmlctx; + XML_Parser parser; + svn_ra_serf__handler_t *handler; + + svn_error_t *inner_error; + + /* Do not use this pool for allocation. It is merely recorded for running + the cleanup handler. */ + apr_pool_t *cleanup_pool; +}; + + +static const apr_uint32_t serf_failure_map[][2] = +{ + { SERF_SSL_CERT_NOTYETVALID, SVN_AUTH_SSL_NOTYETVALID }, + { SERF_SSL_CERT_EXPIRED, SVN_AUTH_SSL_EXPIRED }, + { SERF_SSL_CERT_SELF_SIGNED, SVN_AUTH_SSL_UNKNOWNCA }, + { SERF_SSL_CERT_UNKNOWNCA, SVN_AUTH_SSL_UNKNOWNCA } +}; + +/* Return a Subversion failure mask based on FAILURES, a serf SSL + failure mask. If anything in FAILURES is not directly mappable to + Subversion failures, set SVN_AUTH_SSL_OTHER in the returned mask. */ +static apr_uint32_t +ssl_convert_serf_failures(int failures) +{ + apr_uint32_t svn_failures = 0; + apr_size_t i; + + for (i = 0; i < sizeof(serf_failure_map) / (2 * sizeof(apr_uint32_t)); ++i) + { + if (failures & serf_failure_map[i][0]) + { + svn_failures |= serf_failure_map[i][1]; + failures &= ~serf_failure_map[i][0]; + } + } + + /* Map any remaining failure bits to our OTHER bit. */ + if (failures) + { + svn_failures |= SVN_AUTH_SSL_OTHER; + } + + return svn_failures; +} + + +static apr_status_t +save_error(svn_ra_serf__session_t *session, + svn_error_t *err) +{ + if (err || session->pending_error) + { + session->pending_error = svn_error_compose_create( + session->pending_error, + err); + return session->pending_error->apr_err; + } + + return APR_SUCCESS; +} + + +/* Construct the realmstring, e.g. https://svn.collab.net:443. */ +static const char * +construct_realm(svn_ra_serf__session_t *session, + apr_pool_t *pool) +{ + const char *realm; + apr_port_t port; + + if (session->session_url.port_str) + { + port = session->session_url.port; + } + else + { + port = apr_uri_port_of_scheme(session->session_url.scheme); + } + + realm = apr_psprintf(pool, "%s://%s:%d", + session->session_url.scheme, + session->session_url.hostname, + port); + + return realm; +} + +/* Convert a hash table containing the fields (as documented in X.509) of an + organisation to a string ORG, allocated in POOL. ORG is as returned by + serf_ssl_cert_issuer() and serf_ssl_cert_subject(). */ +static char * +convert_organisation_to_str(apr_hash_t *org, apr_pool_t *pool) +{ + return apr_psprintf(pool, "%s, %s, %s, %s, %s (%s)", + (char*)svn_hash_gets(org, "OU"), + (char*)svn_hash_gets(org, "O"), + (char*)svn_hash_gets(org, "L"), + (char*)svn_hash_gets(org, "ST"), + (char*)svn_hash_gets(org, "C"), + (char*)svn_hash_gets(org, "E")); +} + +/* This function is called on receiving a ssl certificate of a server when + opening a https connection. It allows Subversion to override the initial + validation done by serf. + Serf provides us the @a baton as provided in the call to + serf_ssl_server_cert_callback_set. The result of serf's initial validation + of the certificate @a CERT is returned as a bitmask in FAILURES. */ +static svn_error_t * +ssl_server_cert(void *baton, int failures, + const serf_ssl_certificate_t *cert, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__connection_t *conn = baton; + svn_auth_ssl_server_cert_info_t cert_info; + svn_auth_cred_ssl_server_trust_t *server_creds = NULL; + svn_auth_iterstate_t *state; + const char *realmstring; + apr_uint32_t svn_failures; + apr_hash_t *issuer, *subject, *serf_cert; + apr_array_header_t *san; + void *creds; + int found_matching_hostname = 0; + + /* Implicitly approve any non-server certs. */ + if (serf_ssl_cert_depth(cert) > 0) + { + if (failures) + conn->server_cert_failures |= ssl_convert_serf_failures(failures); + return APR_SUCCESS; + } + + /* Extract the info from the certificate */ + subject = serf_ssl_cert_subject(cert, scratch_pool); + issuer = serf_ssl_cert_issuer(cert, scratch_pool); + serf_cert = serf_ssl_cert_certificate(cert, scratch_pool); + + cert_info.hostname = svn_hash_gets(subject, "CN"); + san = svn_hash_gets(serf_cert, "subjectAltName"); + cert_info.fingerprint = svn_hash_gets(serf_cert, "sha1"); + if (! cert_info.fingerprint) + cert_info.fingerprint = apr_pstrdup(scratch_pool, "<unknown>"); + cert_info.valid_from = svn_hash_gets(serf_cert, "notBefore"); + if (! cert_info.valid_from) + cert_info.valid_from = apr_pstrdup(scratch_pool, "[invalid date]"); + cert_info.valid_until = svn_hash_gets(serf_cert, "notAfter"); + if (! cert_info.valid_until) + cert_info.valid_until = apr_pstrdup(scratch_pool, "[invalid date]"); + cert_info.issuer_dname = convert_organisation_to_str(issuer, scratch_pool); + cert_info.ascii_cert = serf_ssl_cert_export(cert, scratch_pool); + + svn_failures = (ssl_convert_serf_failures(failures) + | conn->server_cert_failures); + + /* Try to find matching server name via subjectAltName first... */ + if (san) { + int i; + for (i = 0; i < san->nelts; i++) { + char *s = APR_ARRAY_IDX(san, i, char*); + if (apr_fnmatch(s, conn->session->session_url.hostname, + APR_FNM_PERIOD) == APR_SUCCESS) { + found_matching_hostname = 1; + cert_info.hostname = s; + break; + } + } + } + + /* Match server certificate CN with the hostname of the server */ + if (!found_matching_hostname && cert_info.hostname) + { + if (apr_fnmatch(cert_info.hostname, conn->session->session_url.hostname, + APR_FNM_PERIOD) == APR_FNM_NOMATCH) + { + svn_failures |= SVN_AUTH_SSL_CNMISMATCH; + } + } + + svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_FAILURES, + &svn_failures); + + svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, + &cert_info); + + realmstring = construct_realm(conn->session, conn->session->pool); + + SVN_ERR(svn_auth_first_credentials(&creds, &state, + SVN_AUTH_CRED_SSL_SERVER_TRUST, + realmstring, + conn->session->wc_callbacks->auth_baton, + scratch_pool)); + if (creds) + { + server_creds = creds; + SVN_ERR(svn_auth_save_credentials(state, scratch_pool)); + } + + svn_auth_set_parameter(conn->session->wc_callbacks->auth_baton, + SVN_AUTH_PARAM_SSL_SERVER_CERT_INFO, NULL); + + if (!server_creds) + return svn_error_create(SVN_ERR_RA_SERF_SSL_CERT_UNTRUSTED, NULL, NULL); + + return SVN_NO_ERROR; +} + +/* Implements serf_ssl_need_server_cert_t for ssl_server_cert */ +static apr_status_t +ssl_server_cert_cb(void *baton, int failures, + const serf_ssl_certificate_t *cert) +{ + svn_ra_serf__connection_t *conn = baton; + svn_ra_serf__session_t *session = conn->session; + apr_pool_t *subpool; + svn_error_t *err; + + subpool = svn_pool_create(session->pool); + err = svn_error_trace(ssl_server_cert(baton, failures, cert, subpool)); + svn_pool_destroy(subpool); + + return save_error(session, err); +} + +static svn_error_t * +load_authorities(svn_ra_serf__connection_t *conn, const char *authorities, + apr_pool_t *pool) +{ + apr_array_header_t *files = svn_cstring_split(authorities, ";", + TRUE /* chop_whitespace */, + pool); + int i; + + for (i = 0; i < files->nelts; ++i) + { + const char *file = APR_ARRAY_IDX(files, i, const char *); + serf_ssl_certificate_t *ca_cert; + apr_status_t status = serf_ssl_load_cert_file(&ca_cert, file, pool); + + if (status == APR_SUCCESS) + status = serf_ssl_trust_cert(conn->ssl_context, ca_cert); + + if (status != APR_SUCCESS) + { + return svn_error_createf(SVN_ERR_BAD_CONFIG_VALUE, NULL, + _("Invalid config: unable to load certificate file '%s'"), + svn_dirent_local_style(file, pool)); + } + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +conn_setup(apr_socket_t *sock, + serf_bucket_t **read_bkt, + serf_bucket_t **write_bkt, + void *baton, + apr_pool_t *pool) +{ + svn_ra_serf__connection_t *conn = baton; + + *read_bkt = serf_context_bucket_socket_create(conn->session->context, + sock, conn->bkt_alloc); + + if (conn->session->using_ssl) + { + /* input stream */ + *read_bkt = serf_bucket_ssl_decrypt_create(*read_bkt, conn->ssl_context, + conn->bkt_alloc); + if (!conn->ssl_context) + { + conn->ssl_context = serf_bucket_ssl_encrypt_context_get(*read_bkt); + +#if SERF_VERSION_AT_LEAST(1,0,0) + serf_ssl_set_hostname(conn->ssl_context, + conn->session->session_url.hostname); +#endif + + serf_ssl_client_cert_provider_set(conn->ssl_context, + svn_ra_serf__handle_client_cert, + conn, conn->session->pool); + serf_ssl_client_cert_password_set(conn->ssl_context, + svn_ra_serf__handle_client_cert_pw, + conn, conn->session->pool); + serf_ssl_server_cert_callback_set(conn->ssl_context, + ssl_server_cert_cb, + conn); + + /* See if the user wants us to trust "default" openssl CAs. */ + if (conn->session->trust_default_ca) + { + serf_ssl_use_default_certificates(conn->ssl_context); + } + /* Are there custom CAs to load? */ + if (conn->session->ssl_authorities) + { + SVN_ERR(load_authorities(conn, conn->session->ssl_authorities, + conn->session->pool)); + } + } + + if (write_bkt) + { + /* output stream */ + *write_bkt = serf_bucket_ssl_encrypt_create(*write_bkt, + conn->ssl_context, + conn->bkt_alloc); + } + } + + return SVN_NO_ERROR; +} + +/* svn_ra_serf__conn_setup is a callback for serf. This function + creates a read bucket and will wrap the write bucket if SSL + is needed. */ +apr_status_t +svn_ra_serf__conn_setup(apr_socket_t *sock, + serf_bucket_t **read_bkt, + serf_bucket_t **write_bkt, + void *baton, + apr_pool_t *pool) +{ + svn_ra_serf__connection_t *conn = baton; + svn_ra_serf__session_t *session = conn->session; + svn_error_t *err; + + err = svn_error_trace(conn_setup(sock, + read_bkt, + write_bkt, + baton, + pool)); + return save_error(session, err); +} + + +/* Our default serf response acceptor. */ +static serf_bucket_t * +accept_response(serf_request_t *request, + serf_bucket_t *stream, + void *acceptor_baton, + apr_pool_t *pool) +{ + serf_bucket_t *c; + serf_bucket_alloc_t *bkt_alloc; + + bkt_alloc = serf_request_get_alloc(request); + c = serf_bucket_barrier_create(stream, bkt_alloc); + + return serf_bucket_response_create(c, bkt_alloc); +} + + +/* Custom response acceptor for HEAD requests. */ +static serf_bucket_t * +accept_head(serf_request_t *request, + serf_bucket_t *stream, + void *acceptor_baton, + apr_pool_t *pool) +{ + serf_bucket_t *response; + + response = accept_response(request, stream, acceptor_baton, pool); + + /* We know we shouldn't get a response body. */ + serf_bucket_response_set_head(response); + + return response; +} + +static svn_error_t * +connection_closed(svn_ra_serf__connection_t *conn, + apr_status_t why, + apr_pool_t *pool) +{ + if (why) + { + SVN_ERR_MALFUNCTION(); + } + + if (conn->session->using_ssl) + conn->ssl_context = NULL; + + return SVN_NO_ERROR; +} + +void +svn_ra_serf__conn_closed(serf_connection_t *conn, + void *closed_baton, + apr_status_t why, + apr_pool_t *pool) +{ + svn_ra_serf__connection_t *ra_conn = closed_baton; + svn_error_t *err; + + err = svn_error_trace(connection_closed(ra_conn, why, pool)); + + (void) save_error(ra_conn->session, err); +} + + +/* Implementation of svn_ra_serf__handle_client_cert */ +static svn_error_t * +handle_client_cert(void *data, + const char **cert_path, + apr_pool_t *pool) +{ + svn_ra_serf__connection_t *conn = data; + svn_ra_serf__session_t *session = conn->session; + const char *realm; + void *creds; + + *cert_path = NULL; + + realm = construct_realm(session, session->pool); + + if (!conn->ssl_client_auth_state) + { + SVN_ERR(svn_auth_first_credentials(&creds, + &conn->ssl_client_auth_state, + SVN_AUTH_CRED_SSL_CLIENT_CERT, + realm, + session->wc_callbacks->auth_baton, + pool)); + } + else + { + SVN_ERR(svn_auth_next_credentials(&creds, + conn->ssl_client_auth_state, + session->pool)); + } + + if (creds) + { + svn_auth_cred_ssl_client_cert_t *client_creds; + client_creds = creds; + *cert_path = client_creds->cert_file; + } + + return SVN_NO_ERROR; +} + +/* Implements serf_ssl_need_client_cert_t for handle_client_cert */ +apr_status_t svn_ra_serf__handle_client_cert(void *data, + const char **cert_path) +{ + svn_ra_serf__connection_t *conn = data; + svn_ra_serf__session_t *session = conn->session; + svn_error_t *err; + + err = svn_error_trace(handle_client_cert(data, cert_path, session->pool)); + + return save_error(session, err); +} + +/* Implementation for svn_ra_serf__handle_client_cert_pw */ +static svn_error_t * +handle_client_cert_pw(void *data, + const char *cert_path, + const char **password, + apr_pool_t *pool) +{ + svn_ra_serf__connection_t *conn = data; + svn_ra_serf__session_t *session = conn->session; + void *creds; + + *password = NULL; + + if (!conn->ssl_client_pw_auth_state) + { + SVN_ERR(svn_auth_first_credentials(&creds, + &conn->ssl_client_pw_auth_state, + SVN_AUTH_CRED_SSL_CLIENT_CERT_PW, + cert_path, + session->wc_callbacks->auth_baton, + pool)); + } + else + { + SVN_ERR(svn_auth_next_credentials(&creds, + conn->ssl_client_pw_auth_state, + pool)); + } + + if (creds) + { + svn_auth_cred_ssl_client_cert_pw_t *pw_creds; + pw_creds = creds; + *password = pw_creds->password; + } + + return APR_SUCCESS; +} + +/* Implements serf_ssl_need_client_cert_pw_t for handle_client_cert_pw */ +apr_status_t svn_ra_serf__handle_client_cert_pw(void *data, + const char *cert_path, + const char **password) +{ + svn_ra_serf__connection_t *conn = data; + svn_ra_serf__session_t *session = conn->session; + svn_error_t *err; + + err = svn_error_trace(handle_client_cert_pw(data, + cert_path, + password, + session->pool)); + + return save_error(session, err); +} + + +/* + * Given a REQUEST on connection CONN, construct a request bucket for it, + * returning the bucket in *REQ_BKT. + * + * If HDRS_BKT is not-NULL, it will be set to a headers_bucket that + * corresponds to the new request. + * + * The request will be METHOD at URL. + * + * If BODY_BKT is not-NULL, it will be sent as the request body. + * + * If CONTENT_TYPE is not-NULL, it will be sent as the Content-Type header. + * + * REQUEST_POOL should live for the duration of the request. Serf will + * construct this and provide it to the request_setup callback, so we + * should just use that one. + */ +static svn_error_t * +setup_serf_req(serf_request_t *request, + serf_bucket_t **req_bkt, + serf_bucket_t **hdrs_bkt, + svn_ra_serf__session_t *session, + const char *method, const char *url, + serf_bucket_t *body_bkt, const char *content_type, + const char *accept_encoding, + apr_pool_t *request_pool, + apr_pool_t *scratch_pool) +{ + serf_bucket_alloc_t *allocator = serf_request_get_alloc(request); + +#if SERF_VERSION_AT_LEAST(1, 1, 0) + svn_spillbuf_t *buf; + + if (session->http10 && body_bkt != NULL) + { + /* Ugh. Use HTTP/1.0 to talk to the server because we don't know if + it speaks HTTP/1.1 (and thus, chunked requests), or because the + server actually responded as only supporting HTTP/1.0. + + We'll take the existing body_bkt, spool it into a spillbuf, and + then wrap a bucket around that spillbuf. The spillbuf will give + us the Content-Length value. */ + SVN_ERR(svn_ra_serf__copy_into_spillbuf(&buf, body_bkt, + request_pool, + scratch_pool)); + /* Destroy original bucket since it content is already copied + to spillbuf. */ + serf_bucket_destroy(body_bkt); + + body_bkt = svn_ra_serf__create_sb_bucket(buf, allocator, + request_pool, + scratch_pool); + } +#endif + + /* Create a request bucket. Note that this sucker is kind enough to + add a "Host" header for us. */ + *req_bkt = serf_request_bucket_request_create(request, method, url, + body_bkt, allocator); + + /* Set the Content-Length value. This will also trigger an HTTP/1.0 + request (rather than the default chunked request). */ +#if SERF_VERSION_AT_LEAST(1, 1, 0) + if (session->http10) + { + if (body_bkt == NULL) + serf_bucket_request_set_CL(*req_bkt, 0); + else + serf_bucket_request_set_CL(*req_bkt, svn_spillbuf__get_size(buf)); + } +#endif + + *hdrs_bkt = serf_bucket_request_get_headers(*req_bkt); + + /* We use serf_bucket_headers_setn() because the USERAGENT has a + lifetime longer than this bucket. Thus, there is no need to copy + the header values. */ + serf_bucket_headers_setn(*hdrs_bkt, "User-Agent", session->useragent); + + if (content_type) + { + serf_bucket_headers_setn(*hdrs_bkt, "Content-Type", content_type); + } + +#if SERF_VERSION_AT_LEAST(1, 1, 0) + if (session->http10) + serf_bucket_headers_setn(*hdrs_bkt, "Connection", "keep-alive"); +#endif + + if (accept_encoding) + { + serf_bucket_headers_setn(*hdrs_bkt, "Accept-Encoding", accept_encoding); + } + + /* These headers need to be sent with every request; see issue #3255 + ("mod_dav_svn does not pass client capabilities to start-commit + hooks") for why. */ + serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_DEPTH); + serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_MERGEINFO); + serf_bucket_headers_setn(*hdrs_bkt, "DAV", SVN_DAV_NS_DAV_SVN_LOG_REVPROPS); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__context_run_wait(svn_boolean_t *done, + svn_ra_serf__session_t *sess, + apr_pool_t *scratch_pool) +{ + apr_pool_t *iterpool; + apr_interval_time_t waittime_left = sess->timeout; + + assert(sess->pending_error == SVN_NO_ERROR); + + iterpool = svn_pool_create(scratch_pool); + while (!*done) + { + apr_status_t status; + svn_error_t *err; + int i; + + svn_pool_clear(iterpool); + + if (sess->cancel_func) + SVN_ERR((*sess->cancel_func)(sess->cancel_baton)); + + status = serf_context_run(sess->context, + SVN_RA_SERF__CONTEXT_RUN_DURATION, + iterpool); + + err = sess->pending_error; + sess->pending_error = SVN_NO_ERROR; + + /* If the context duration timeout is up, we'll subtract that + duration from the total time alloted for such things. If + there's no time left, we fail with a message indicating that + the connection timed out. */ + if (APR_STATUS_IS_TIMEUP(status)) + { + svn_error_clear(err); + err = SVN_NO_ERROR; + status = 0; + + if (sess->timeout) + { + if (waittime_left > SVN_RA_SERF__CONTEXT_RUN_DURATION) + { + waittime_left -= SVN_RA_SERF__CONTEXT_RUN_DURATION; + } + else + { + return svn_error_create(SVN_ERR_RA_DAV_CONN_TIMEOUT, NULL, + _("Connection timed out")); + } + } + } + else + { + waittime_left = sess->timeout; + } + + SVN_ERR(err); + if (status) + { + if (status >= SVN_ERR_BAD_CATEGORY_START && status < SVN_ERR_LAST) + { + /* apr can't translate subversion errors to text */ + SVN_ERR_W(svn_error_create(status, NULL, NULL), + _("Error running context")); + } + + return svn_ra_serf__wrap_err(status, _("Error running context")); + } + + /* Debugging purposes only! */ + for (i = 0; i < sess->num_conns; i++) + { + serf_debug__closed_conn(sess->conns[i]->bkt_alloc); + } + } + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__context_run_one(svn_ra_serf__handler_t *handler, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + + /* Create a serf request based on HANDLER. */ + svn_ra_serf__request_create(handler); + + /* Wait until the response logic marks its DONE status. */ + err = svn_ra_serf__context_run_wait(&handler->done, handler->session, + scratch_pool); + if (handler->server_error) + { + err = svn_error_compose_create(err, handler->server_error->error); + handler->server_error = NULL; + } + + return svn_error_trace(err); +} + + +/* + * Expat callback invoked on a start element tag for an error response. + */ +static svn_error_t * +start_error(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (!ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "error") == 0) + { + ctx->in_error = TRUE; + } + else if (ctx->in_error && strcmp(name.name, "human-readable") == 0) + { + const char *err_code; + + err_code = svn_xml_get_attr_value("errcode", attrs); + if (err_code) + { + apr_int64_t val; + + SVN_ERR(svn_cstring_atoi64(&val, err_code)); + ctx->error->apr_err = (apr_status_t)val; + } + + /* If there's no error code provided, or if the provided code is + 0 (which can happen sometimes depending on how the error is + constructed on the server-side), just pick a generic error + code to run with. */ + if (! ctx->error->apr_err) + { + ctx->error->apr_err = SVN_ERR_RA_DAV_REQUEST_FAILED; + } + + /* Start collecting cdata. */ + svn_stringbuf_setempty(ctx->cdata); + ctx->collect_cdata = TRUE; + } + + return SVN_NO_ERROR; +} + +/* + * Expat callback invoked on an end element tag for a PROPFIND response. + */ +static svn_error_t * +end_error(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "error") == 0) + { + ctx->in_error = FALSE; + } + if (ctx->in_error && strcmp(name.name, "human-readable") == 0) + { + /* On the server dav_error_response_tag() will add a leading + and trailing newline if DEBUG_CR is defined in mod_dav.h, + so remove any such characters here. */ + svn_stringbuf_strip_whitespace(ctx->cdata); + + ctx->error->message = apr_pstrmemdup(ctx->error->pool, ctx->cdata->data, + ctx->cdata->len); + ctx->collect_cdata = FALSE; + } + + return SVN_NO_ERROR; +} + +/* + * Expat callback invoked on CDATA elements in an error response. + * + * This callback can be called multiple times. + */ +static svn_error_t * +cdata_error(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (ctx->collect_cdata) + { + svn_stringbuf_appendbytes(ctx->cdata, data, len); + } + + return SVN_NO_ERROR; +} + + +static apr_status_t +drain_bucket(serf_bucket_t *bucket) +{ + /* Read whatever is in the bucket, and just drop it. */ + while (1) + { + apr_status_t status; + const char *data; + apr_size_t len; + + status = serf_bucket_read(bucket, SERF_READ_ALL_AVAIL, &data, &len); + if (status) + return status; + } +} + + +static svn_ra_serf__server_error_t * +begin_error_parsing(svn_ra_serf__xml_start_element_t start, + svn_ra_serf__xml_end_element_t end, + svn_ra_serf__xml_cdata_chunk_handler_t cdata, + apr_pool_t *result_pool) +{ + svn_ra_serf__server_error_t *server_err; + + server_err = apr_pcalloc(result_pool, sizeof(*server_err)); + server_err->error = svn_error_create(APR_SUCCESS, NULL, NULL); + server_err->contains_precondition_error = FALSE; + server_err->cdata = svn_stringbuf_create_empty(server_err->error->pool); + server_err->collect_cdata = FALSE; + server_err->parser.pool = server_err->error->pool; + server_err->parser.user_data = server_err; + server_err->parser.start = start; + server_err->parser.end = end; + server_err->parser.cdata = cdata; + server_err->parser.ignore_errors = TRUE; + + return server_err; +} + +/* Implements svn_ra_serf__response_handler_t */ +svn_error_t * +svn_ra_serf__handle_discard_body(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool) +{ + apr_status_t status; + + status = drain_bucket(response); + if (status) + return svn_ra_serf__wrap_err(status, NULL); + + return SVN_NO_ERROR; +} + +apr_status_t +svn_ra_serf__response_discard_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool) +{ + return drain_bucket(response); +} + + +/* Return the value of the RESPONSE's Location header if any, or NULL + otherwise. */ +static const char * +response_get_location(serf_bucket_t *response, + const char *base_url, + apr_pool_t *result_pool, + apr_pool_t *scratch_pool) +{ + serf_bucket_t *headers; + const char *location; + + headers = serf_bucket_response_get_headers(response); + location = serf_bucket_headers_get(headers, "Location"); + if (location == NULL) + return NULL; + + /* The RFCs say we should have received a full url in LOCATION, but + older apache versions and many custom web handlers just return a + relative path here... + + And we can't trust anything because it is network data. + */ + if (*location == '/') + { + apr_uri_t uri; + apr_status_t status; + + status = apr_uri_parse(scratch_pool, base_url, &uri); + + if (status != APR_SUCCESS) + return NULL; + + /* Replace the path path with what we got */ + uri.path = (char*)svn_urlpath__canonicalize(location, scratch_pool); + + /* And make APR produce a proper full url for us */ + location = apr_uri_unparse(scratch_pool, &uri, 0); + + /* Fall through to ensure our canonicalization rules */ + } + else if (!svn_path_is_url(location)) + { + return NULL; /* Any other formats we should support? */ + } + + return svn_uri_canonicalize(location, result_pool); +} + + +/* Implements svn_ra_serf__response_handler_t */ +svn_error_t * +svn_ra_serf__expect_empty_body(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t *handler = baton; + serf_bucket_t *hdrs; + const char *val; + + /* This function is just like handle_multistatus_only() except for the + XML parsing callbacks. We want to look for the human-readable element. */ + + /* We should see this just once, in order to initialize SERVER_ERROR. + At that point, the core error processing will take over. If we choose + not to parse an error, then we'll never return here (because we + change the response handler). */ + SVN_ERR_ASSERT(handler->server_error == NULL); + + hdrs = serf_bucket_response_get_headers(response); + val = serf_bucket_headers_get(hdrs, "Content-Type"); + if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0) + { + svn_ra_serf__server_error_t *server_err; + + server_err = begin_error_parsing(start_error, end_error, cdata_error, + handler->handler_pool); + + /* Get the parser to set our DONE flag. */ + server_err->parser.done = &handler->done; + + handler->server_error = server_err; + } + else + { + /* The body was not text/xml, so we don't know what to do with it. + Toss anything that arrives. */ + handler->discard_body = TRUE; + } + + /* Returning SVN_NO_ERROR will return APR_SUCCESS to serf, which tells it + to call the response handler again. That will start up the XML parsing, + or it will be dropped on the floor (per the decision above). */ + return SVN_NO_ERROR; +} + + +/* Given a string like "HTTP/1.1 500 (status)" in BUF, parse out the numeric + status code into *STATUS_CODE_OUT. Ignores leading whitespace. */ +static svn_error_t * +parse_dav_status(int *status_code_out, svn_stringbuf_t *buf, + apr_pool_t *scratch_pool) +{ + svn_error_t *err; + const char *token; + char *tok_status; + svn_stringbuf_t *temp_buf = svn_stringbuf_dup(buf, scratch_pool); + + svn_stringbuf_strip_whitespace(temp_buf); + token = apr_strtok(temp_buf->data, " \t\r\n", &tok_status); + if (token) + token = apr_strtok(NULL, " \t\r\n", &tok_status); + if (!token) + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("Malformed DAV:status CDATA '%s'"), + buf->data); + err = svn_cstring_atoi(status_code_out, token); + if (err) + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, err, + _("Malformed DAV:status CDATA '%s'"), + buf->data); + + return SVN_NO_ERROR; +} + +/* + * Expat callback invoked on a start element tag for a 207 response. + */ +static svn_error_t * +start_207(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + const char **attrs, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (!ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "multistatus") == 0) + { + ctx->in_error = TRUE; + } + else if (ctx->in_error && strcmp(name.name, "responsedescription") == 0) + { + /* Start collecting cdata. */ + svn_stringbuf_setempty(ctx->cdata); + ctx->collect_cdata = TRUE; + } + else if (ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "status") == 0) + { + /* Start collecting cdata. */ + svn_stringbuf_setempty(ctx->cdata); + ctx->collect_cdata = TRUE; + } + + return SVN_NO_ERROR; +} + +/* + * Expat callback invoked on an end element tag for a 207 response. + */ +static svn_error_t * +end_207(svn_ra_serf__xml_parser_t *parser, + svn_ra_serf__dav_props_t name, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "multistatus") == 0) + { + ctx->in_error = FALSE; + } + if (ctx->in_error && strcmp(name.name, "responsedescription") == 0) + { + /* Remove leading newline added by DEBUG_CR on server */ + svn_stringbuf_strip_whitespace(ctx->cdata); + + ctx->collect_cdata = FALSE; + ctx->error->message = apr_pstrmemdup(ctx->error->pool, ctx->cdata->data, + ctx->cdata->len); + if (ctx->contains_precondition_error) + ctx->error->apr_err = SVN_ERR_FS_PROP_BASEVALUE_MISMATCH; + else + ctx->error->apr_err = SVN_ERR_RA_DAV_REQUEST_FAILED; + } + else if (ctx->in_error && + strcmp(name.namespace, "DAV:") == 0 && + strcmp(name.name, "status") == 0) + { + int status_code; + + ctx->collect_cdata = FALSE; + + SVN_ERR(parse_dav_status(&status_code, ctx->cdata, parser->pool)); + if (status_code == 412) + ctx->contains_precondition_error = TRUE; + } + + return SVN_NO_ERROR; +} + +/* + * Expat callback invoked on CDATA elements in a 207 response. + * + * This callback can be called multiple times. + */ +static svn_error_t * +cdata_207(svn_ra_serf__xml_parser_t *parser, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t *ctx = parser->user_data; + + if (ctx->collect_cdata) + { + svn_stringbuf_appendbytes(ctx->cdata, data, len); + } + + return SVN_NO_ERROR; +} + +/* Implements svn_ra_serf__response_handler_t */ +svn_error_t * +svn_ra_serf__handle_multistatus_only(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t *handler = baton; + + /* This function is just like expect_empty_body() except for the + XML parsing callbacks. We are looking for very limited pieces of + the multistatus response. */ + + /* We should see this just once, in order to initialize SERVER_ERROR. + At that point, the core error processing will take over. If we choose + not to parse an error, then we'll never return here (because we + change the response handler). */ + SVN_ERR_ASSERT(handler->server_error == NULL); + + { + serf_bucket_t *hdrs; + const char *val; + + hdrs = serf_bucket_response_get_headers(response); + val = serf_bucket_headers_get(hdrs, "Content-Type"); + if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0) + { + svn_ra_serf__server_error_t *server_err; + + server_err = begin_error_parsing(start_207, end_207, cdata_207, + handler->handler_pool); + + /* Get the parser to set our DONE flag. */ + server_err->parser.done = &handler->done; + + handler->server_error = server_err; + } + else + { + /* The body was not text/xml, so we don't know what to do with it. + Toss anything that arrives. */ + handler->discard_body = TRUE; + } + } + + /* Returning SVN_NO_ERROR will return APR_SUCCESS to serf, which tells it + to call the response handler again. That will start up the XML parsing, + or it will be dropped on the floor (per the decision above). */ + return SVN_NO_ERROR; +} + + +/* Conforms to Expat's XML_StartElementHandler */ +static void +start_xml(void *userData, const char *raw_name, const char **attrs) +{ + svn_ra_serf__xml_parser_t *parser = userData; + svn_ra_serf__dav_props_t name; + apr_pool_t *scratch_pool; + svn_error_t *err; + + if (parser->error) + return; + + if (!parser->state) + svn_ra_serf__xml_push_state(parser, 0); + + /* ### get a real scratch_pool */ + scratch_pool = parser->state->pool; + + svn_ra_serf__define_ns(&parser->state->ns_list, attrs, parser->state->pool); + + svn_ra_serf__expand_ns(&name, parser->state->ns_list, raw_name); + + err = parser->start(parser, name, attrs, scratch_pool); + if (err && !SERF_BUCKET_READ_ERROR(err->apr_err)) + err = svn_error_create(SVN_ERR_RA_SERF_WRAPPED_ERROR, err, NULL); + + parser->error = err; +} + + +/* Conforms to Expat's XML_EndElementHandler */ +static void +end_xml(void *userData, const char *raw_name) +{ + svn_ra_serf__xml_parser_t *parser = userData; + svn_ra_serf__dav_props_t name; + svn_error_t *err; + apr_pool_t *scratch_pool; + + if (parser->error) + return; + + /* ### get a real scratch_pool */ + scratch_pool = parser->state->pool; + + svn_ra_serf__expand_ns(&name, parser->state->ns_list, raw_name); + + err = parser->end(parser, name, scratch_pool); + if (err && !SERF_BUCKET_READ_ERROR(err->apr_err)) + err = svn_error_create(SVN_ERR_RA_SERF_WRAPPED_ERROR, err, NULL); + + parser->error = err; +} + + +/* Conforms to Expat's XML_CharacterDataHandler */ +static void +cdata_xml(void *userData, const char *data, int len) +{ + svn_ra_serf__xml_parser_t *parser = userData; + svn_error_t *err; + apr_pool_t *scratch_pool; + + if (parser->error) + return; + + if (!parser->state) + svn_ra_serf__xml_push_state(parser, 0); + + /* ### get a real scratch_pool */ + scratch_pool = parser->state->pool; + + err = parser->cdata(parser, data, len, scratch_pool); + if (err && !SERF_BUCKET_READ_ERROR(err->apr_err)) + err = svn_error_create(SVN_ERR_RA_SERF_WRAPPED_ERROR, err, NULL); + + parser->error = err; +} + +/* Flip the requisite bits in CTX to indicate that processing of the + response is complete, adding the current "done item" to the list of + completed items. */ +static void +add_done_item(svn_ra_serf__xml_parser_t *ctx) +{ + /* Make sure we don't add to DONE_LIST twice. */ + if (!*ctx->done) + { + *ctx->done = TRUE; + if (ctx->done_list) + { + ctx->done_item->data = ctx->user_data; + ctx->done_item->next = *ctx->done_list; + *ctx->done_list = ctx->done_item; + } + } +} + + +static svn_error_t * +write_to_pending(svn_ra_serf__xml_parser_t *ctx, + const char *data, + apr_size_t len, + apr_pool_t *scratch_pool) +{ + if (ctx->pending == NULL) + { + ctx->pending = apr_pcalloc(ctx->pool, sizeof(*ctx->pending)); + ctx->pending->buf = svn_spillbuf__create(PARSE_CHUNK_SIZE, + SPILL_SIZE, + ctx->pool); + } + + /* Copy the data into one or more chunks in the spill buffer. */ + return svn_error_trace(svn_spillbuf__write(ctx->pending->buf, + data, len, + scratch_pool)); +} + + +static svn_error_t * +inject_to_parser(svn_ra_serf__xml_parser_t *ctx, + const char *data, + apr_size_t len, + const serf_status_line *sl) +{ + int xml_status; + + xml_status = XML_Parse(ctx->xmlp, data, (int) len, 0); + if (xml_status == XML_STATUS_ERROR && !ctx->ignore_errors) + { + if (sl == NULL) + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("XML parsing failed")); + + return svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, NULL, + _("XML parsing failed: (%d %s)"), + sl->code, sl->reason); + } + + if (ctx->error && !ctx->ignore_errors) + return svn_error_trace(ctx->error); + + return SVN_NO_ERROR; +} + +/* Apr pool cleanup handler to release an XML_Parser in success and error + conditions */ +static apr_status_t +xml_parser_cleanup(void *baton) +{ + XML_Parser *xmlp = baton; + + if (*xmlp) + { + (void) XML_ParserFree(*xmlp); + *xmlp = NULL; + } + + return APR_SUCCESS; +} + +/* Limit the amount of pending content to parse at once to < 100KB per + iteration. This number is chosen somewhat arbitrarely. Making it lower + will have a drastical negative impact on performance, whereas increasing it + increases the risk for connection timeouts. + */ +#define PENDING_TO_PARSE PARSE_CHUNK_SIZE * 5 + +svn_error_t * +svn_ra_serf__process_pending(svn_ra_serf__xml_parser_t *parser, + svn_boolean_t *network_eof, + apr_pool_t *scratch_pool) +{ + svn_boolean_t pending_empty = FALSE; + apr_size_t cur_read = 0; + + /* Fast path exit: already paused, nothing to do, or already done. */ + if (parser->paused || parser->pending == NULL || *parser->done) + { + *network_eof = parser->pending ? parser->pending->network_eof : FALSE; + return SVN_NO_ERROR; + } + + /* Parsing the pending conten in the spillbuf will result in many disc i/o + operations. This can be so slow that we don't run the network event + processing loop often enough, resulting in timed out connections. + + So we limit the amounts of bytes parsed per iteration. + */ + while (cur_read < PENDING_TO_PARSE) + { + const char *data; + apr_size_t len; + + /* Get a block of content, stopping the loop when we run out. */ + SVN_ERR(svn_spillbuf__read(&data, &len, parser->pending->buf, + scratch_pool)); + if (data) + { + /* Inject the content into the XML parser. */ + SVN_ERR(inject_to_parser(parser, data, len, NULL)); + + /* If the XML parsing callbacks paused us, then we're done for now. */ + if (parser->paused) + break; + + cur_read += len; + } + else + { + /* The buffer is empty. */ + pending_empty = TRUE; + break; + } + } + + /* If the PENDING structures are empty *and* we consumed all content from + the network, then we're completely done with the parsing. */ + if (pending_empty && + parser->pending->network_eof) + { + SVN_ERR_ASSERT(parser->xmlp != NULL); + + /* Tell the parser that no more content will be parsed. Ignore the + return status. We just don't care. */ + (void) XML_Parse(parser->xmlp, NULL, 0, 1); + + apr_pool_cleanup_run(parser->pool, &parser->xmlp, xml_parser_cleanup); + parser->xmlp = NULL; + add_done_item(parser); + } + + *network_eof = parser->pending ? parser->pending->network_eof : FALSE; + + return SVN_NO_ERROR; +} +#undef PENDING_TO_PARSE + + +/* ### this is still broken conceptually. just shifting incrementally... */ +static svn_error_t * +handle_server_error(serf_request_t *request, + serf_bucket_t *response, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__server_error_t server_err = { 0 }; + serf_bucket_t *hdrs; + const char *val; + apr_status_t err; + + hdrs = serf_bucket_response_get_headers(response); + val = serf_bucket_headers_get(hdrs, "Content-Type"); + if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0) + { + /* ### we should figure out how to reuse begin_error_parsing */ + + server_err.error = svn_error_create(APR_SUCCESS, NULL, NULL); + server_err.contains_precondition_error = FALSE; + server_err.cdata = svn_stringbuf_create_empty(scratch_pool); + server_err.collect_cdata = FALSE; + server_err.parser.pool = server_err.error->pool; + server_err.parser.user_data = &server_err; + server_err.parser.start = start_error; + server_err.parser.end = end_error; + server_err.parser.cdata = cdata_error; + server_err.parser.done = &server_err.done; + server_err.parser.ignore_errors = TRUE; + + /* We don't care about any errors except for SERVER_ERR.ERROR */ + svn_error_clear(svn_ra_serf__handle_xml_parser(request, + response, + &server_err.parser, + scratch_pool)); + + /* ### checking DONE is silly. the above only parses whatever has + ### been received at the network interface. totally wrong. but + ### it is what we have for now (maintaining historical code), + ### until we fully migrate. */ + if (server_err.done && server_err.error->apr_err == APR_SUCCESS) + { + svn_error_clear(server_err.error); + server_err.error = SVN_NO_ERROR; + } + + return svn_error_trace(server_err.error); + } + + /* The only error that we will return is from the XML response body. + Otherwise, ignore the entire body but allow SUCCESS/EOF/EAGAIN to + surface. */ + err = drain_bucket(response); + if (err && !SERF_BUCKET_READ_ERROR(err)) + return svn_ra_serf__wrap_err(err, NULL); + + return SVN_NO_ERROR; +} + + +/* Implements svn_ra_serf__response_handler_t */ +svn_error_t * +svn_ra_serf__handle_xml_parser(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *pool) +{ + serf_status_line sl; + apr_status_t status; + svn_ra_serf__xml_parser_t *ctx = baton; + svn_error_t *err; + + /* ### get the HANDLER rather than fetching this. */ + status = serf_bucket_response_status(response, &sl); + if (SERF_BUCKET_READ_ERROR(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + + /* Woo-hoo. Nothing here to see. */ + if (sl.code == 404 && !ctx->ignore_errors) + { + err = handle_server_error(request, response, pool); + + if (err && APR_STATUS_IS_EOF(err->apr_err)) + add_done_item(ctx); + + return svn_error_trace(err); + } + + if (ctx->headers_baton == NULL) + ctx->headers_baton = serf_bucket_response_get_headers(response); + else if (ctx->headers_baton != serf_bucket_response_get_headers(response)) + { + /* We got a new response to an existing parser... + This tells us the connection has restarted and we should continue + where we stopped last time. + */ + + /* Is this a second attempt?? */ + if (!ctx->skip_size) + ctx->skip_size = ctx->read_size; + + ctx->read_size = 0; /* New request, nothing read */ + } + + if (!ctx->xmlp) + { + ctx->xmlp = XML_ParserCreate(NULL); + apr_pool_cleanup_register(ctx->pool, &ctx->xmlp, xml_parser_cleanup, + apr_pool_cleanup_null); + XML_SetUserData(ctx->xmlp, ctx); + XML_SetElementHandler(ctx->xmlp, start_xml, end_xml); + if (ctx->cdata) + { + XML_SetCharacterDataHandler(ctx->xmlp, cdata_xml); + } + } + + while (1) + { + const char *data; + apr_size_t len; + + status = serf_bucket_read(response, PARSE_CHUNK_SIZE, &data, &len); + + if (SERF_BUCKET_READ_ERROR(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + + ctx->read_size += len; + + if (ctx->skip_size) + { + /* Handle restarted requests correctly: Skip what we already read */ + apr_size_t skip; + + if (ctx->skip_size >= ctx->read_size) + { + /* Eek. What did the file shrink or something? */ + if (APR_STATUS_IS_EOF(status)) + { + SVN_ERR_MALFUNCTION(); + } + + /* Skip on to the next iteration of this loop. */ + if (APR_STATUS_IS_EAGAIN(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + continue; + } + + skip = (apr_size_t)(len - (ctx->read_size - ctx->skip_size)); + data += skip; + len -= skip; + ctx->skip_size = 0; + } + + /* Note: once the callbacks invoked by inject_to_parser() sets the + PAUSED flag, then it will not be cleared. write_to_pending() will + only save the content. Logic outside of serf_context_run() will + clear that flag, as appropriate, along with processing the + content that we have placed into the PENDING buffer. + + We want to save arriving content into the PENDING structures if + the parser has been paused, or we already have data in there (so + the arriving data is appended, rather than injected out of order) */ + if (ctx->paused || HAS_PENDING_DATA(ctx->pending)) + { + err = write_to_pending(ctx, data, len, pool); + } + else + { + err = inject_to_parser(ctx, data, len, &sl); + if (err) + { + /* Should have no errors if IGNORE_ERRORS is set. */ + SVN_ERR_ASSERT(!ctx->ignore_errors); + } + } + if (err) + { + SVN_ERR_ASSERT(ctx->xmlp != NULL); + + apr_pool_cleanup_run(ctx->pool, &ctx->xmlp, xml_parser_cleanup); + add_done_item(ctx); + return svn_error_trace(err); + } + + if (APR_STATUS_IS_EAGAIN(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + + if (APR_STATUS_IS_EOF(status)) + { + if (ctx->pending != NULL) + ctx->pending->network_eof = TRUE; + + /* We just hit the end of the network content. If we have nothing + in the PENDING structures, then we're completely done. */ + if (!HAS_PENDING_DATA(ctx->pending)) + { + SVN_ERR_ASSERT(ctx->xmlp != NULL); + + /* Ignore the return status. We just don't care. */ + (void) XML_Parse(ctx->xmlp, NULL, 0, 1); + + apr_pool_cleanup_run(ctx->pool, &ctx->xmlp, xml_parser_cleanup); + add_done_item(ctx); + } + + return svn_ra_serf__wrap_err(status, NULL); + } + + /* feed me! */ + } + /* not reached */ +} + + +apr_status_t +svn_ra_serf__credentials_callback(char **username, char **password, + serf_request_t *request, void *baton, + int code, const char *authn_type, + const char *realm, + apr_pool_t *pool) +{ + svn_ra_serf__handler_t *handler = baton; + svn_ra_serf__session_t *session = handler->session; + void *creds; + svn_auth_cred_simple_t *simple_creds; + svn_error_t *err; + + if (code == 401) + { + /* Use svn_auth_first_credentials if this is the first time we ask for + credentials during this session OR if the last time we asked + session->auth_state wasn't set (eg. if the credentials provider was + cancelled by the user). */ + if (!session->auth_state) + { + err = svn_auth_first_credentials(&creds, + &session->auth_state, + SVN_AUTH_CRED_SIMPLE, + realm, + session->wc_callbacks->auth_baton, + session->pool); + } + else + { + err = svn_auth_next_credentials(&creds, + session->auth_state, + session->pool); + } + + if (err) + { + (void) save_error(session, err); + return err->apr_err; + } + + session->auth_attempts++; + + if (!creds || session->auth_attempts > 4) + { + /* No more credentials. */ + (void) save_error(session, + svn_error_create( + SVN_ERR_AUTHN_FAILED, NULL, + _("No more credentials or we tried too many " + "times.\nAuthentication failed"))); + return SVN_ERR_AUTHN_FAILED; + } + + simple_creds = creds; + *username = apr_pstrdup(pool, simple_creds->username); + *password = apr_pstrdup(pool, simple_creds->password); + } + else + { + *username = apr_pstrdup(pool, session->proxy_username); + *password = apr_pstrdup(pool, session->proxy_password); + + session->proxy_auth_attempts++; + + if (!session->proxy_username || session->proxy_auth_attempts > 4) + { + /* No more credentials. */ + (void) save_error(session, + svn_error_create( + SVN_ERR_AUTHN_FAILED, NULL, + _("Proxy authentication failed"))); + return SVN_ERR_AUTHN_FAILED; + } + } + + handler->conn->last_status_code = code; + + return APR_SUCCESS; +} + +/* Wait for HTTP response status and headers, and invoke HANDLER-> + response_handler() to carry out operation-specific processing. + Afterwards, check for connection close. + + SERF_STATUS allows returning errors to serf without creating a + subversion error object. + */ +static svn_error_t * +handle_response(serf_request_t *request, + serf_bucket_t *response, + svn_ra_serf__handler_t *handler, + apr_status_t *serf_status, + apr_pool_t *scratch_pool) +{ + apr_status_t status; + svn_error_t *err; + + /* ### need to verify whether this already gets init'd on every + ### successful exit. for an error-exit, it will (properly) be + ### ignored by the caller. */ + *serf_status = APR_SUCCESS; + + if (!response) + { + /* Uh-oh. Our connection died. */ + if (handler->response_error) + SVN_ERR(handler->response_error(request, response, 0, + handler->response_error_baton)); + + /* Requeue another request for this handler. + ### how do we know if the handler can deal with this?! */ + svn_ra_serf__request_create(handler); + + return SVN_NO_ERROR; + } + + /* If we're reading the body, then skip all this preparation. */ + if (handler->reading_body) + goto process_body; + + /* Copy the Status-Line info into HANDLER, if we don't yet have it. */ + if (handler->sline.version == 0) + { + serf_status_line sl; + + status = serf_bucket_response_status(response, &sl); + if (status != APR_SUCCESS) + { + /* The response line is not (yet) ready, or some other error. */ + *serf_status = status; + return SVN_NO_ERROR; /* Handled by serf */ + } + + /* If we got APR_SUCCESS, then we should have Status-Line info. */ + SVN_ERR_ASSERT(sl.version != 0); + + handler->sline = sl; + handler->sline.reason = apr_pstrdup(handler->handler_pool, sl.reason); + + /* HTTP/1.1? (or later) */ + if (sl.version != SERF_HTTP_10) + handler->session->http10 = FALSE; + } + + /* Keep reading from the network until we've read all the headers. */ + status = serf_bucket_response_wait_for_headers(response); + if (status) + { + /* The typical "error" will be APR_EAGAIN, meaning that more input + from the network is required to complete the reading of the + headers. */ + if (!APR_STATUS_IS_EOF(status)) + { + /* Either the headers are not (yet) complete, or there really + was an error. */ + *serf_status = status; + return SVN_NO_ERROR; + } + + /* wait_for_headers() will return EOF if there is no body in this + response, or if we completely read the body. The latter is not + true since we would have set READING_BODY to get the body read, + and we would not be back to this code block. + + It can also return EOF if we truly hit EOF while (say) processing + the headers. aka Badness. */ + + /* Cases where a lack of a response body (via EOF) is okay: + * - A HEAD request + * - 204/304 response + * + * Otherwise, if we get an EOF here, something went really wrong: either + * the server closed on us early or we're reading too much. Either way, + * scream loudly. + */ + if (strcmp(handler->method, "HEAD") != 0 + && handler->sline.code != 204 + && handler->sline.code != 304) + { + err = svn_error_createf(SVN_ERR_RA_DAV_MALFORMED_DATA, + svn_ra_serf__wrap_err(status, NULL), + _("Premature EOF seen from server" + " (http status=%d)"), + handler->sline.code); + + /* In case anything else arrives... discard it. */ + handler->discard_body = TRUE; + + return err; + } + } + + /* ... and set up the header fields in HANDLER. */ + handler->location = response_get_location(response, + handler->session->session_url_str, + handler->handler_pool, + scratch_pool); + + /* On the last request, we failed authentication. We succeeded this time, + so let's save away these credentials. */ + if (handler->conn->last_status_code == 401 && handler->sline.code < 400) + { + SVN_ERR(svn_auth_save_credentials(handler->session->auth_state, + handler->session->pool)); + handler->session->auth_attempts = 0; + handler->session->auth_state = NULL; + } + handler->conn->last_status_code = handler->sline.code; + + if (handler->sline.code == 405 + || handler->sline.code == 408 + || handler->sline.code == 409 + || handler->sline.code >= 500) + { + /* 405 Method Not allowed. + 408 Request Timeout + 409 Conflict: can indicate a hook error. + 5xx (Internal) Server error. */ + serf_bucket_t *hdrs; + const char *val; + + hdrs = serf_bucket_response_get_headers(response); + val = serf_bucket_headers_get(hdrs, "Content-Type"); + if (val && strncasecmp(val, "text/xml", sizeof("text/xml") - 1) == 0) + { + svn_ra_serf__server_error_t *server_err; + + server_err = begin_error_parsing(start_error, end_error, cdata_error, + handler->handler_pool); + /* Get the parser to set our DONE flag. */ + server_err->parser.done = &handler->done; + + handler->server_error = server_err; + } + else + { + handler->discard_body = TRUE; + + if (!handler->session->pending_error) + { + apr_status_t apr_err = SVN_ERR_RA_DAV_REQUEST_FAILED; + + /* 405 == Method Not Allowed (Occurs when trying to lock a working + copy path which no longer exists at HEAD in the repository. */ + if (handler->sline.code == 405 + && strcmp(handler->method, "LOCK") == 0) + apr_err = SVN_ERR_FS_OUT_OF_DATE; + + handler->session->pending_error = + svn_error_createf(apr_err, NULL, + _("%s request on '%s' failed: %d %s"), + handler->method, handler->path, + handler->sline.code, handler->sline.reason); + } + } + } + + /* Stop processing the above, on every packet arrival. */ + handler->reading_body = TRUE; + + process_body: + + /* We've been instructed to ignore the body. Drain whatever is present. */ + if (handler->discard_body) + { + *serf_status = drain_bucket(response); + + /* If the handler hasn't set done (which it shouldn't have) and + we now have the EOF, go ahead and set it so that we can stop + our context loops. + */ + if (!handler->done && APR_STATUS_IS_EOF(*serf_status)) + handler->done = TRUE; + + return SVN_NO_ERROR; + } + + /* If we are supposed to parse the body as a server_error, then do + that now. */ + if (handler->server_error != NULL) + { + err = svn_ra_serf__handle_xml_parser(request, response, + &handler->server_error->parser, + scratch_pool); + + /* If we do not receive an error or it is a non-transient error, return + immediately. + + APR_EOF will be returned when parsing is complete. + + APR_EAGAIN & WAIT_CONN may be intermittently returned as we proceed through + parsing and the network has no more data right now. If we receive that, + clear the error and return - allowing serf to wait for more data. + */ + if (!err || SERF_BUCKET_READ_ERROR(err->apr_err)) + return svn_error_trace(err); + + if (!APR_STATUS_IS_EOF(err->apr_err)) + { + *serf_status = err->apr_err; + svn_error_clear(err); + return SVN_NO_ERROR; + } + + /* Clear the EOF. We don't need it. */ + svn_error_clear(err); + + /* If the parsing is done, and we did not extract an error, then + simply toss everything, and anything else that might arrive. + The higher-level code will need to investigate HANDLER->SLINE, + as we have no further information for them. */ + if (handler->done + && handler->server_error->error->apr_err == APR_SUCCESS) + { + svn_error_clear(handler->server_error->error); + + /* Stop parsing for a server error. */ + handler->server_error = NULL; + + /* If anything arrives after this, then just discard it. */ + handler->discard_body = TRUE; + } + + *serf_status = APR_EOF; + return SVN_NO_ERROR; + } + + /* Pass the body along to the registered response handler. */ + err = handler->response_handler(request, response, + handler->response_baton, + scratch_pool); + + if (err + && (!SERF_BUCKET_READ_ERROR(err->apr_err) + || APR_STATUS_IS_ECONNRESET(err->apr_err) + || APR_STATUS_IS_ECONNABORTED(err->apr_err))) + { + /* These errors are special cased in serf + ### We hope no handler returns these by accident. */ + *serf_status = err->apr_err; + svn_error_clear(err); + return SVN_NO_ERROR; + } + + return svn_error_trace(err); +} + + +/* Implements serf_response_handler_t for handle_response. Storing + errors in handler->session->pending_error if appropriate. */ +static apr_status_t +handle_response_cb(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + svn_ra_serf__handler_t *handler = baton; + svn_error_t *err; + apr_status_t inner_status; + apr_status_t outer_status; + + err = svn_error_trace(handle_response(request, response, + handler, &inner_status, + scratch_pool)); + + /* Select the right status value to return. */ + outer_status = save_error(handler->session, err); + if (!outer_status) + outer_status = inner_status; + + /* Make sure the DONE flag is set properly. */ + if (APR_STATUS_IS_EOF(outer_status) || APR_STATUS_IS_EOF(inner_status)) + handler->done = TRUE; + + return outer_status; +} + +/* Perform basic request setup, with special handling for HEAD requests, + and finer-grained callbacks invoked (if non-NULL) to produce the request + headers and body. */ +static svn_error_t * +setup_request(serf_request_t *request, + svn_ra_serf__handler_t *handler, + serf_bucket_t **req_bkt, + apr_pool_t *request_pool, + apr_pool_t *scratch_pool) +{ + serf_bucket_t *body_bkt; + serf_bucket_t *headers_bkt; + const char *accept_encoding; + + if (handler->body_delegate) + { + serf_bucket_alloc_t *bkt_alloc = serf_request_get_alloc(request); + + /* ### should pass the scratch_pool */ + SVN_ERR(handler->body_delegate(&body_bkt, handler->body_delegate_baton, + bkt_alloc, request_pool)); + } + else + { + body_bkt = NULL; + } + + if (handler->custom_accept_encoding) + { + accept_encoding = NULL; + } + else if (handler->session->using_compression) + { + /* Accept gzip compression if enabled. */ + accept_encoding = "gzip"; + } + else + { + accept_encoding = NULL; + } + + SVN_ERR(setup_serf_req(request, req_bkt, &headers_bkt, + handler->session, handler->method, handler->path, + body_bkt, handler->body_type, accept_encoding, + request_pool, scratch_pool)); + + if (handler->header_delegate) + { + /* ### should pass the scratch_pool */ + SVN_ERR(handler->header_delegate(headers_bkt, + handler->header_delegate_baton, + request_pool)); + } + + return APR_SUCCESS; +} + +/* Implements the serf_request_setup_t interface (which sets up both a + request and its response handler callback). Handles errors for + setup_request_cb */ +static apr_status_t +setup_request_cb(serf_request_t *request, + void *setup_baton, + serf_bucket_t **req_bkt, + serf_response_acceptor_t *acceptor, + void **acceptor_baton, + serf_response_handler_t *s_handler, + void **s_handler_baton, + apr_pool_t *pool) +{ + svn_ra_serf__handler_t *handler = setup_baton; + svn_error_t *err; + + /* ### construct a scratch_pool? serf gives us a pool that will live for + ### the duration of the request. */ + apr_pool_t *scratch_pool = pool; + + if (strcmp(handler->method, "HEAD") == 0) + *acceptor = accept_head; + else + *acceptor = accept_response; + *acceptor_baton = handler->session; + + *s_handler = handle_response_cb; + *s_handler_baton = handler; + + err = svn_error_trace(setup_request(request, handler, req_bkt, + pool /* request_pool */, scratch_pool)); + + return save_error(handler->session, err); +} + +void +svn_ra_serf__request_create(svn_ra_serf__handler_t *handler) +{ + SVN_ERR_ASSERT_NO_RETURN(handler->handler_pool != NULL); + + /* In case HANDLER is re-queued, reset the various transient fields. + + ### prior to recent changes, HANDLER was constant. maybe we should + ### break out these processing fields, apart from the request + ### definition. */ + handler->done = FALSE; + handler->server_error = NULL; + handler->sline.version = 0; + handler->location = NULL; + handler->reading_body = FALSE; + handler->discard_body = FALSE; + + /* ### do we ever alter the >response_handler? */ + + /* ### do we need to hold onto the returned request object, or just + ### not worry about it (the serf ctx will manage it). */ + (void) serf_connection_request_create(handler->conn->conn, + setup_request_cb, handler); +} + + +svn_error_t * +svn_ra_serf__discover_vcc(const char **vcc_url, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool) +{ + const char *path; + const char *relative_path; + const char *uuid; + + /* If we've already got the information our caller seeks, just return it. */ + if (session->vcc_url && session->repos_root_str) + { + *vcc_url = session->vcc_url; + return SVN_NO_ERROR; + } + + /* If no connection is provided, use the default one. */ + if (! conn) + { + conn = session->conns[0]; + } + + path = session->session_url.path; + *vcc_url = NULL; + uuid = NULL; + + do + { + apr_hash_t *props; + svn_error_t *err; + + err = svn_ra_serf__fetch_node_props(&props, conn, + path, SVN_INVALID_REVNUM, + base_props, pool, pool); + if (! err) + { + apr_hash_t *ns_props; + + ns_props = apr_hash_get(props, "DAV:", 4); + *vcc_url = svn_prop_get_value(ns_props, + "version-controlled-configuration"); + + ns_props = svn_hash_gets(props, SVN_DAV_PROP_NS_DAV); + relative_path = svn_prop_get_value(ns_props, + "baseline-relative-path"); + uuid = svn_prop_get_value(ns_props, "repository-uuid"); + break; + } + else + { + if ((err->apr_err != SVN_ERR_FS_NOT_FOUND) && + (err->apr_err != SVN_ERR_RA_DAV_FORBIDDEN)) + { + return svn_error_trace(err); /* found a _real_ error */ + } + else + { + /* This happens when the file is missing in HEAD. */ + svn_error_clear(err); + + /* Okay, strip off a component from PATH. */ + path = svn_urlpath__dirname(path, pool); + + /* An error occurred on conns. serf 0.4.0 remembers that + the connection had a problem. We need to reset it, in + order to use it again. */ + serf_connection_reset(conn->conn); + } + } + } + while ((path[0] != '\0') + && (! (path[0] == '/' && path[1] == '\0'))); + + if (!*vcc_url) + { + return svn_error_create(SVN_ERR_RA_DAV_OPTIONS_REQ_FAILED, NULL, + _("The PROPFIND response did not include the " + "requested version-controlled-configuration " + "value")); + } + + /* Store our VCC in our cache. */ + if (!session->vcc_url) + { + session->vcc_url = apr_pstrdup(session->pool, *vcc_url); + } + + /* Update our cached repository root URL. */ + if (!session->repos_root_str) + { + svn_stringbuf_t *url_buf; + + url_buf = svn_stringbuf_create(path, pool); + + svn_path_remove_components(url_buf, + svn_path_component_count(relative_path)); + + /* Now recreate the root_url. */ + session->repos_root = session->session_url; + session->repos_root.path = + (char *)svn_fspath__canonicalize(url_buf->data, session->pool); + session->repos_root_str = + svn_urlpath__canonicalize(apr_uri_unparse(session->pool, + &session->repos_root, 0), + session->pool); + } + + /* Store the repository UUID in the cache. */ + if (!session->uuid) + { + session->uuid = apr_pstrdup(session->pool, uuid); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__get_relative_path(const char **rel_path, + const char *orig_path, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool) +{ + const char *decoded_root, *decoded_orig; + + if (! session->repos_root.path) + { + const char *vcc_url; + + /* This should only happen if we haven't detected HTTP v2 + support from the server. */ + assert(! SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)); + + /* We don't actually care about the VCC_URL, but this API + promises to populate the session's root-url cache, and that's + what we really want. */ + SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, + conn ? conn : session->conns[0], + pool)); + } + + decoded_root = svn_path_uri_decode(session->repos_root.path, pool); + decoded_orig = svn_path_uri_decode(orig_path, pool); + *rel_path = svn_urlpath__skip_ancestor(decoded_root, decoded_orig); + SVN_ERR_ASSERT(*rel_path != NULL); + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__report_resource(const char **report_target, + svn_ra_serf__session_t *session, + svn_ra_serf__connection_t *conn, + apr_pool_t *pool) +{ + /* If we have HTTP v2 support, we want to report against the 'me' + resource. */ + if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session)) + *report_target = apr_pstrdup(pool, session->me_resource); + + /* Otherwise, we'll use the default VCC. */ + else + SVN_ERR(svn_ra_serf__discover_vcc(report_target, session, conn, pool)); + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__error_on_status(int status_code, + const char *path, + const char *location) +{ + switch(status_code) + { + case 301: + case 302: + case 307: + return svn_error_createf(SVN_ERR_RA_DAV_RELOCATED, NULL, + (status_code == 301) + ? _("Repository moved permanently to '%s';" + " please relocate") + : _("Repository moved temporarily to '%s';" + " please relocate"), location); + case 403: + return svn_error_createf(SVN_ERR_RA_DAV_FORBIDDEN, NULL, + _("Access to '%s' forbidden"), path); + + case 404: + return svn_error_createf(SVN_ERR_FS_NOT_FOUND, NULL, + _("'%s' path not found"), path); + case 423: + return svn_error_createf(SVN_ERR_FS_NO_LOCK_TOKEN, NULL, + _("'%s': no lock token available"), path); + } + + return SVN_NO_ERROR; +} + +svn_error_t * +svn_ra_serf__register_editor_shim_callbacks(svn_ra_session_t *ra_session, + svn_delta_shim_callbacks_t *callbacks) +{ + svn_ra_serf__session_t *session = ra_session->priv; + + session->shim_callbacks = callbacks; + return SVN_NO_ERROR; +} + + +/* Conforms to Expat's XML_StartElementHandler */ +static void +expat_start(void *userData, const char *raw_name, const char **attrs) +{ + struct expat_ctx_t *ectx = userData; + + if (ectx->inner_error != NULL) + return; + + ectx->inner_error = svn_error_trace( + svn_ra_serf__xml_cb_start(ectx->xmlctx, + raw_name, attrs)); + +#ifdef EXPAT_HAS_STOPPARSER + if (ectx->inner_error) + (void) XML_StopParser(ectx->parser, 0 /* resumable */); +#endif +} + + +/* Conforms to Expat's XML_EndElementHandler */ +static void +expat_end(void *userData, const char *raw_name) +{ + struct expat_ctx_t *ectx = userData; + + if (ectx->inner_error != NULL) + return; + + ectx->inner_error = svn_error_trace( + svn_ra_serf__xml_cb_end(ectx->xmlctx, raw_name)); + +#ifdef EXPAT_HAS_STOPPARSER + if (ectx->inner_error) + (void) XML_StopParser(ectx->parser, 0 /* resumable */); +#endif +} + + +/* Conforms to Expat's XML_CharacterDataHandler */ +static void +expat_cdata(void *userData, const char *data, int len) +{ + struct expat_ctx_t *ectx = userData; + + if (ectx->inner_error != NULL) + return; + + ectx->inner_error = svn_error_trace( + svn_ra_serf__xml_cb_cdata(ectx->xmlctx, data, len)); + +#ifdef EXPAT_HAS_STOPPARSER + if (ectx->inner_error) + (void) XML_StopParser(ectx->parser, 0 /* resumable */); +#endif +} + + +/* Implements svn_ra_serf__response_handler_t */ +static svn_error_t * +expat_response_handler(serf_request_t *request, + serf_bucket_t *response, + void *baton, + apr_pool_t *scratch_pool) +{ + struct expat_ctx_t *ectx = baton; + + if (!ectx->parser) + { + ectx->parser = XML_ParserCreate(NULL); + apr_pool_cleanup_register(ectx->cleanup_pool, &ectx->parser, + xml_parser_cleanup, apr_pool_cleanup_null); + XML_SetUserData(ectx->parser, ectx); + XML_SetElementHandler(ectx->parser, expat_start, expat_end); + XML_SetCharacterDataHandler(ectx->parser, expat_cdata); + } + + /* ### TODO: sline.code < 200 should really be handled by the core */ + if ((ectx->handler->sline.code < 200) || (ectx->handler->sline.code >= 300)) + { + /* By deferring to expect_empty_body(), it will make a choice on + how to handle the body. Whatever the decision, the core handler + will take over, and we will not be called again. */ + return svn_error_trace(svn_ra_serf__expect_empty_body( + request, response, ectx->handler, + scratch_pool)); + } + + while (1) + { + apr_status_t status; + const char *data; + apr_size_t len; + int expat_status; + + status = serf_bucket_read(response, PARSE_CHUNK_SIZE, &data, &len); + if (SERF_BUCKET_READ_ERROR(status)) + return svn_ra_serf__wrap_err(status, NULL); + +#if 0 + /* ### move restart/skip into the core handler */ + ectx->handler->read_size += len; +#endif + + /* ### move PAUSED behavior to a new response handler that can feed + ### an inner handler, or can pause for a while. */ + + /* ### should we have an IGNORE_ERRORS flag like the v1 parser? */ + + expat_status = XML_Parse(ectx->parser, data, (int)len, 0 /* isFinal */); + + /* We need to check INNER_ERROR first. This is an error from the + callbacks that has been "dropped off" for us to retrieve. On + current Expat parsers, we stop the parser when an error occurs, + so we want to ignore EXPAT_STATUS (which reports the stoppage). + + If an error is not present, THEN we go ahead and look for parsing + errors. */ + if (ectx->inner_error) + { + apr_pool_cleanup_run(ectx->cleanup_pool, &ectx->parser, + xml_parser_cleanup); + return svn_error_trace(ectx->inner_error); + } + if (expat_status == XML_STATUS_ERROR) + return svn_error_createf(SVN_ERR_XML_MALFORMED, + ectx->inner_error, + _("The %s response contains invalid XML" + " (%d %s)"), + ectx->handler->method, + ectx->handler->sline.code, + ectx->handler->sline.reason); + + /* The parsing went fine. What has the bucket told us? */ + + if (APR_STATUS_IS_EOF(status)) + { + /* Tell expat we've reached the end of the content. Ignore the + return status. We just don't care. */ + (void) XML_Parse(ectx->parser, NULL, 0, 1 /* isFinal */); + + svn_ra_serf__xml_context_destroy(ectx->xmlctx); + apr_pool_cleanup_run(ectx->cleanup_pool, &ectx->parser, + xml_parser_cleanup); + + /* ### should check XMLCTX to see if it has returned to the + ### INITIAL state. we may have ended early... */ + } + + if (status && !SERF_BUCKET_READ_ERROR(status)) + { + return svn_ra_serf__wrap_err(status, NULL); + } + } + + /* NOTREACHED */ +} + + +svn_ra_serf__handler_t * +svn_ra_serf__create_expat_handler(svn_ra_serf__xml_context_t *xmlctx, + apr_pool_t *result_pool) +{ + svn_ra_serf__handler_t *handler; + struct expat_ctx_t *ectx; + + ectx = apr_pcalloc(result_pool, sizeof(*ectx)); + ectx->xmlctx = xmlctx; + ectx->parser = NULL; + ectx->cleanup_pool = result_pool; + + + handler = apr_pcalloc(result_pool, sizeof(*handler)); + handler->handler_pool = result_pool; + handler->response_handler = expat_response_handler; + handler->response_baton = ectx; + + ectx->handler = handler; + + return handler; +} diff --git a/subversion/libsvn_ra_serf/util_error.c b/subversion/libsvn_ra_serf/util_error.c new file mode 100644 index 0000000..da66091 --- /dev/null +++ b/subversion/libsvn_ra_serf/util_error.c @@ -0,0 +1,100 @@ +/* + * util_error.c : serf utility routines for wrapping serf status codes + * + * ==================================================================== + * 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 <serf.h> + +#include "svn_utf.h" +#include "private/svn_error_private.h" + +#include "ra_serf.h" + +/* + * Undefine the helpers for creating errors. + * + * *NOTE*: Any use of these functions in any other function may need + * to call svn_error__locate() because the macro that would otherwise + * do this is being undefined and the filename and line number will + * not be properly set in the static error_file and error_line + * variables. + */ +#undef svn_error_create +#undef svn_error_createf +#undef svn_error_quick_wrap +#undef svn_error_wrap_apr +#undef svn_ra_serf__wrap_err + +svn_error_t * +svn_ra_serf__wrap_err(apr_status_t status, + const char *fmt, + ...) +{ + const char *serf_err_msg = serf_error_string(status); + svn_error_t *err; + va_list ap; + + err = svn_error_create(status, NULL, NULL); + + if (serf_err_msg || fmt) + { + const char *msg; + const char *err_msg; + char errbuf[255]; /* Buffer for APR error message. */ + + if (serf_err_msg) + { + err_msg = serf_err_msg; + } + else + { + svn_error_t *utf8_err; + + /* Grab the APR error message. */ + apr_strerror(status, errbuf, sizeof(errbuf)); + utf8_err = svn_utf_cstring_to_utf8(&err_msg, errbuf, err->pool); + if (utf8_err) + err_msg = NULL; + svn_error_clear(utf8_err); + } + + /* Append it to the formatted message. */ + if (fmt) + { + va_start(ap, fmt); + msg = apr_pvsprintf(err->pool, fmt, ap); + va_end(ap); + } + else + { + msg = "ra_serf"; + } + if (err_msg) + { + err->message = apr_pstrcat(err->pool, msg, ": ", err_msg, NULL); + } + else + { + err->message = msg; + } + } + + return err; +} diff --git a/subversion/libsvn_ra_serf/xml.c b/subversion/libsvn_ra_serf/xml.c new file mode 100644 index 0000000..95017d5 --- /dev/null +++ b/subversion/libsvn_ra_serf/xml.c @@ -0,0 +1,825 @@ +/* + * xml.c : standard XML parsing routines for ra_serf + * + * ==================================================================== + * 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_uri.h> +#include <serf.h> + +#include "svn_hash.h" +#include "svn_pools.h" +#include "svn_ra.h" +#include "svn_dav.h" +#include "svn_xml.h" +#include "../libsvn_ra/ra_loader.h" +#include "svn_config.h" +#include "svn_delta.h" +#include "svn_path.h" + +#include "svn_private_config.h" +#include "private/svn_string_private.h" + +#include "ra_serf.h" + + +struct svn_ra_serf__xml_context_t { + /* Current state information. */ + svn_ra_serf__xml_estate_t *current; + + /* If WAITING.NAMESPACE != NULL, wait for NAMESPACE:NAME element to be + closed before looking for transitions from CURRENT->STATE. */ + svn_ra_serf__dav_props_t waiting; + + /* The transition table. */ + const svn_ra_serf__xml_transition_t *ttable; + + /* The callback information. */ + svn_ra_serf__xml_opened_t opened_cb; + svn_ra_serf__xml_closed_t closed_cb; + svn_ra_serf__xml_cdata_t cdata_cb; + void *baton; + + /* Linked list of free states. */ + svn_ra_serf__xml_estate_t *free_states; + +#ifdef SVN_DEBUG + /* Used to verify we are not re-entering a callback, specifically to + ensure SCRATCH_POOL is not cleared while an outer callback is + trying to use it. */ + svn_boolean_t within_callback; +#define START_CALLBACK(xmlctx) \ + do { \ + svn_ra_serf__xml_context_t *xmlctx__tmp = (xmlctx); \ + SVN_ERR_ASSERT(!xmlctx__tmp->within_callback); \ + xmlctx__tmp->within_callback = TRUE; \ + } while (0) +#define END_CALLBACK(xmlctx) ((xmlctx)->within_callback = FALSE) +#else +#define START_CALLBACK(xmlctx) /* empty */ +#define END_CALLBACK(xmlctx) /* empty */ +#endif /* SVN_DEBUG */ + + apr_pool_t *scratch_pool; + +}; + +struct svn_ra_serf__xml_estate_t { + /* The current state value. */ + int state; + + /* The xml tag that opened this state. Waiting for the tag to close. */ + svn_ra_serf__dav_props_t tag; + + /* Should the CLOSED_CB function be called for custom processing when + this tag is closed? */ + svn_boolean_t custom_close; + + /* A pool may be constructed for this state. */ + apr_pool_t *state_pool; + + /* The namespaces extent for this state/element. This will start with + the parent's NS_LIST, and we will push new namespaces into our + local list. The parent will be unaffected by our locally-scoped data. */ + svn_ra_serf__ns_t *ns_list; + + /* Any collected attribute values. char * -> svn_string_t *. May be NULL + if no attributes have been collected. */ + apr_hash_t *attrs; + + /* Any collected cdata. May be NULL if no cdata is being collected. */ + svn_stringbuf_t *cdata; + + /* Previous/outer state. */ + svn_ra_serf__xml_estate_t *prev; + +}; + + +static void +define_namespaces(svn_ra_serf__ns_t **ns_list, + const char *const *attrs, + apr_pool_t *(*get_pool)(void *baton), + void *baton) +{ + const char *const *tmp_attrs = attrs; + + for (tmp_attrs = attrs; *tmp_attrs != NULL; tmp_attrs += 2) + { + if (strncmp(*tmp_attrs, "xmlns", 5) == 0) + { + const svn_ra_serf__ns_t *cur_ns; + svn_boolean_t found = FALSE; + const char *prefix; + + /* The empty prefix, or a named-prefix. */ + if (tmp_attrs[0][5] == ':') + prefix = &tmp_attrs[0][6]; + else + prefix = ""; + + /* Have we already defined this ns previously? */ + for (cur_ns = *ns_list; cur_ns; cur_ns = cur_ns->next) + { + if (strcmp(cur_ns->namespace, prefix) == 0) + { + found = TRUE; + break; + } + } + + if (!found) + { + apr_pool_t *pool; + svn_ra_serf__ns_t *new_ns; + + if (get_pool) + pool = get_pool(baton); + else + pool = baton; + new_ns = apr_palloc(pool, sizeof(*new_ns)); + new_ns->namespace = apr_pstrdup(pool, prefix); + new_ns->url = apr_pstrdup(pool, tmp_attrs[1]); + + /* Push into the front of NS_LIST. Parent states will point + to later in the chain, so will be unaffected by + shadowing/other namespaces pushed onto NS_LIST. */ + new_ns->next = *ns_list; + *ns_list = new_ns; + } + } + } +} + + +void +svn_ra_serf__define_ns(svn_ra_serf__ns_t **ns_list, + const char *const *attrs, + apr_pool_t *result_pool) +{ + define_namespaces(ns_list, attrs, NULL /* get_pool */, result_pool); +} + + +/* + * Look up NAME in the NS_LIST list for previously declared namespace + * definitions and return a DAV_PROPS_T-tuple that has values. + */ +void +svn_ra_serf__expand_ns(svn_ra_serf__dav_props_t *returned_prop_name, + const svn_ra_serf__ns_t *ns_list, + const char *name) +{ + const char *colon; + + colon = strchr(name, ':'); + if (colon) + { + const svn_ra_serf__ns_t *ns; + + for (ns = ns_list; ns; ns = ns->next) + { + if (strncmp(ns->namespace, name, colon - name) == 0) + { + returned_prop_name->namespace = ns->url; + returned_prop_name->name = colon + 1; + return; + } + } + } + else + { + const svn_ra_serf__ns_t *ns; + + for (ns = ns_list; ns; ns = ns->next) + { + if (! ns->namespace[0]) + { + returned_prop_name->namespace = ns->url; + returned_prop_name->name = name; + return; + } + } + } + + /* If the prefix is not found, then the name is NOT within a + namespace. */ + returned_prop_name->namespace = ""; + returned_prop_name->name = name; +} + + +#define XML_HEADER "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + +void +svn_ra_serf__add_xml_header_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc) +{ + serf_bucket_t *tmp; + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(XML_HEADER, sizeof(XML_HEADER) - 1, + bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); +} + +void +svn_ra_serf__add_open_tag_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *tag, ...) +{ + va_list ap; + const char *key; + serf_bucket_t *tmp; + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("<", 1, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(tag, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + va_start(ap, tag); + while ((key = va_arg(ap, char *)) != NULL) + { + const char *val = va_arg(ap, const char *); + if (val) + { + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(" ", 1, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(key, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("=\"", 2, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(val, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("\"", 1, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + } + } + va_end(ap); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(">", 1, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); +} + +void +svn_ra_serf__add_close_tag_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *tag) +{ + serf_bucket_t *tmp; + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN("</", 2, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING(tag, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); + + tmp = SERF_BUCKET_SIMPLE_STRING_LEN(">", 1, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp); +} + +void +svn_ra_serf__add_cdata_len_buckets(serf_bucket_t *agg_bucket, + serf_bucket_alloc_t *bkt_alloc, + const char *data, apr_size_t len) +{ + const char *end = data + len; + const char *p = data, *q; + serf_bucket_t *tmp_bkt; + + while (1) + { + /* Find a character which needs to be quoted and append bytes up + to that point. Strictly speaking, '>' only needs to be + quoted if it follows "]]", but it's easier to quote it all + the time. + + So, why are we escaping '\r' here? Well, according to the + XML spec, '\r\n' gets converted to '\n' during XML parsing. + Also, any '\r' not followed by '\n' is converted to '\n'. By + golly, if we say we want to escape a '\r', we want to make + sure it remains a '\r'! */ + q = p; + while (q < end && *q != '&' && *q != '<' && *q != '>' && *q != '\r') + q++; + + + tmp_bkt = SERF_BUCKET_SIMPLE_STRING_LEN(p, q - p, bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp_bkt); + + /* We may already be a winner. */ + if (q == end) + break; + + /* Append the entity reference for the character. */ + if (*q == '&') + { + tmp_bkt = SERF_BUCKET_SIMPLE_STRING_LEN("&", sizeof("&") - 1, + bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp_bkt); + } + else if (*q == '<') + { + tmp_bkt = SERF_BUCKET_SIMPLE_STRING_LEN("<", sizeof("<") - 1, + bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp_bkt); + } + else if (*q == '>') + { + tmp_bkt = SERF_BUCKET_SIMPLE_STRING_LEN(">", sizeof(">") - 1, + bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp_bkt); + } + else if (*q == '\r') + { + tmp_bkt = SERF_BUCKET_SIMPLE_STRING_LEN(" ", sizeof(" ") - 1, + bkt_alloc); + serf_bucket_aggregate_append(agg_bucket, tmp_bkt); + } + + p = q + 1; + } +} + +void svn_ra_serf__add_tag_buckets(serf_bucket_t *agg_bucket, const char *tag, + const char *value, + serf_bucket_alloc_t *bkt_alloc) +{ + svn_ra_serf__add_open_tag_buckets(agg_bucket, bkt_alloc, tag, NULL); + + if (value) + { + svn_ra_serf__add_cdata_len_buckets(agg_bucket, bkt_alloc, + value, strlen(value)); + } + + svn_ra_serf__add_close_tag_buckets(agg_bucket, bkt_alloc, tag); +} + +void +svn_ra_serf__xml_push_state(svn_ra_serf__xml_parser_t *parser, + int state) +{ + svn_ra_serf__xml_state_t *new_state; + + if (!parser->free_state) + { + new_state = apr_palloc(parser->pool, sizeof(*new_state)); + new_state->pool = svn_pool_create(parser->pool); + } + else + { + new_state = parser->free_state; + parser->free_state = parser->free_state->prev; + + svn_pool_clear(new_state->pool); + } + + if (parser->state) + { + new_state->private = parser->state->private; + new_state->ns_list = parser->state->ns_list; + } + else + { + new_state->private = NULL; + new_state->ns_list = NULL; + } + + new_state->current_state = state; + + /* Add it to the state chain. */ + new_state->prev = parser->state; + parser->state = new_state; +} + +void svn_ra_serf__xml_pop_state(svn_ra_serf__xml_parser_t *parser) +{ + svn_ra_serf__xml_state_t *cur_state; + + cur_state = parser->state; + parser->state = cur_state->prev; + cur_state->prev = parser->free_state; + parser->free_state = cur_state; +} + + +/* Return a pool for XES to use for self-alloc (and other specifics). */ +static apr_pool_t * +xes_pool(const svn_ra_serf__xml_estate_t *xes) +{ + /* Move up through parent states looking for one with a pool. This + will always terminate since the initial state has a pool. */ + while (xes->state_pool == NULL) + xes = xes->prev; + return xes->state_pool; +} + + +static void +ensure_pool(svn_ra_serf__xml_estate_t *xes) +{ + if (xes->state_pool == NULL) + xes->state_pool = svn_pool_create(xes_pool(xes)); +} + + +/* This callback is used by define_namespaces() to wait until a pool is + required before constructing it. */ +static apr_pool_t * +lazy_create_pool(void *baton) +{ + svn_ra_serf__xml_estate_t *xes = baton; + + ensure_pool(xes); + return xes->state_pool; +} + +void +svn_ra_serf__xml_context_destroy( + svn_ra_serf__xml_context_t *xmlctx) +{ + svn_pool_destroy(xmlctx->scratch_pool); +} + +svn_ra_serf__xml_context_t * +svn_ra_serf__xml_context_create( + const svn_ra_serf__xml_transition_t *ttable, + svn_ra_serf__xml_opened_t opened_cb, + svn_ra_serf__xml_closed_t closed_cb, + svn_ra_serf__xml_cdata_t cdata_cb, + void *baton, + apr_pool_t *result_pool) +{ + svn_ra_serf__xml_context_t *xmlctx; + svn_ra_serf__xml_estate_t *xes; + + xmlctx = apr_pcalloc(result_pool, sizeof(*xmlctx)); + xmlctx->ttable = ttable; + xmlctx->opened_cb = opened_cb; + xmlctx->closed_cb = closed_cb; + xmlctx->cdata_cb = cdata_cb; + xmlctx->baton = baton; + xmlctx->scratch_pool = svn_pool_create(result_pool); + + xes = apr_pcalloc(result_pool, sizeof(*xes)); + /* XES->STATE == 0 */ + + /* Child states may use this pool to allocate themselves. If a child + needs to collect information, then it will construct a subpool and + will use that to allocate itself and its collected data. */ + xes->state_pool = result_pool; + + xmlctx->current = xes; + + return xmlctx; +} + + +apr_hash_t * +svn_ra_serf__xml_gather_since(svn_ra_serf__xml_estate_t *xes, + int stop_state) +{ + apr_hash_t *data; + apr_pool_t *pool; + + ensure_pool(xes); + pool = xes->state_pool; + + data = apr_hash_make(pool); + + for (; xes != NULL; xes = xes->prev) + { + if (xes->attrs != NULL) + { + apr_hash_index_t *hi; + + for (hi = apr_hash_first(pool, xes->attrs); hi; + hi = apr_hash_next(hi)) + { + const void *key; + apr_ssize_t klen; + void *val; + + /* Parent name/value lifetimes are at least as long as POOL. */ + apr_hash_this(hi, &key, &klen, &val); + apr_hash_set(data, key, klen, val); + } + } + + if (xes->state == stop_state) + break; + } + + return data; +} + + +void +svn_ra_serf__xml_note(svn_ra_serf__xml_estate_t *xes, + int state, + const char *name, + const char *value) +{ + svn_ra_serf__xml_estate_t *scan; + + for (scan = xes; scan != NULL && scan->state != state; scan = scan->prev) + /* pass */ ; + + SVN_ERR_ASSERT_NO_RETURN(scan != NULL); + + /* Make sure the target state has a pool. */ + ensure_pool(scan); + + /* ... and attribute storage. */ + if (scan->attrs == NULL) + scan->attrs = apr_hash_make(scan->state_pool); + + /* In all likelihood, NAME is a string constant. But we can't really + be sure. And it isn't like we're storing a billion of these into + the state pool. */ + svn_hash_sets(scan->attrs, + apr_pstrdup(scan->state_pool, name), + apr_pstrdup(scan->state_pool, value)); +} + + +apr_pool_t * +svn_ra_serf__xml_state_pool(svn_ra_serf__xml_estate_t *xes) +{ + /* If they asked for a pool, then ensure that we have one to provide. */ + ensure_pool(xes); + + return xes->state_pool; +} + + +svn_error_t * +svn_ra_serf__xml_cb_start(svn_ra_serf__xml_context_t *xmlctx, + const char *raw_name, + const char *const *attrs) +{ + svn_ra_serf__xml_estate_t *current = xmlctx->current; + svn_ra_serf__dav_props_t elemname; + const svn_ra_serf__xml_transition_t *scan; + apr_pool_t *new_pool; + svn_ra_serf__xml_estate_t *new_xes; + + /* If we're waiting for an element to close, then just ignore all + other element-opens. */ + if (xmlctx->waiting.namespace != NULL) + return SVN_NO_ERROR; + + /* Look for xmlns: attributes. Lazily create the state pool if any + were found. */ + define_namespaces(¤t->ns_list, attrs, lazy_create_pool, current); + + svn_ra_serf__expand_ns(&elemname, current->ns_list, raw_name); + + for (scan = xmlctx->ttable; scan->ns != NULL; ++scan) + { + if (scan->from_state != current->state) + continue; + + /* Wildcard tag match. */ + if (*scan->name == '*') + break; + + /* Found a specific transition. */ + if (strcmp(elemname.name, scan->name) == 0 + && strcmp(elemname.namespace, scan->ns) == 0) + break; + } + if (scan->ns == NULL) + { + xmlctx->waiting = elemname; + /* ### return? */ + return SVN_NO_ERROR; + } + + /* We should not be told to collect cdata if the closed_cb will not + be called. */ + SVN_ERR_ASSERT(!scan->collect_cdata || scan->custom_close); + + /* Found a transition. Make it happen. */ + + /* ### todo. push state */ + + /* ### how to use free states? */ + /* This state should be allocated in the extent pool. If we will be + collecting information for this state, then construct a subpool. + + ### potentially optimize away the subpool if none of the + ### attributes are present. subpools are cheap, tho... */ + new_pool = xes_pool(current); + if (scan->collect_cdata || scan->collect_attrs[0]) + { + new_pool = svn_pool_create(new_pool); + + /* Prep the new state. */ + new_xes = apr_pcalloc(new_pool, sizeof(*new_xes)); + new_xes->state_pool = new_pool; + + /* If we're supposed to collect cdata, then set up a buffer for + this. The existence of this buffer will instruct our cdata + callback to collect the cdata. */ + if (scan->collect_cdata) + new_xes->cdata = svn_stringbuf_create_empty(new_pool); + + if (scan->collect_attrs[0] != NULL) + { + const char *const *saveattr = &scan->collect_attrs[0]; + + new_xes->attrs = apr_hash_make(new_pool); + for (; *saveattr != NULL; ++saveattr) + { + const char *name; + const char *value; + + if (**saveattr == '?') + { + name = *saveattr + 1; + value = svn_xml_get_attr_value(name, attrs); + } + else + { + name = *saveattr; + value = svn_xml_get_attr_value(name, attrs); + if (value == NULL) + return svn_error_createf(SVN_ERR_XML_ATTRIB_NOT_FOUND, + NULL, + _("Missing XML attribute: '%s'"), + name); + } + + if (value) + svn_hash_sets(new_xes->attrs, name, + apr_pstrdup(new_pool, value)); + } + } + } + else + { + /* Prep the new state. */ + new_xes = apr_pcalloc(new_pool, sizeof(*new_xes)); + /* STATE_POOL remains NULL. */ + } + + /* Some basic copies to set up the new estate. */ + new_xes->state = scan->to_state; + new_xes->tag.name = apr_pstrdup(new_pool, elemname.name); + new_xes->tag.namespace = apr_pstrdup(new_pool, elemname.namespace); + new_xes->custom_close = scan->custom_close; + + /* Start with the parent's namespace set. */ + new_xes->ns_list = current->ns_list; + + /* The new state is prepared. Make it current. */ + new_xes->prev = current; + xmlctx->current = new_xes; + + if (xmlctx->opened_cb) + { + START_CALLBACK(xmlctx); + SVN_ERR(xmlctx->opened_cb(new_xes, xmlctx->baton, + new_xes->state, &new_xes->tag, + xmlctx->scratch_pool)); + END_CALLBACK(xmlctx); + svn_pool_clear(xmlctx->scratch_pool); + } + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__xml_cb_end(svn_ra_serf__xml_context_t *xmlctx, + const char *raw_name) +{ + svn_ra_serf__xml_estate_t *xes = xmlctx->current; + svn_ra_serf__dav_props_t elemname; + + svn_ra_serf__expand_ns(&elemname, xes->ns_list, raw_name); + + if (xmlctx->waiting.namespace != NULL) + { + /* If this element is not the closer, then keep waiting... */ + if (strcmp(elemname.name, xmlctx->waiting.name) != 0 + || strcmp(elemname.namespace, xmlctx->waiting.namespace) != 0) + return SVN_NO_ERROR; + + /* Found it. Stop waiting, and go back for more. */ + xmlctx->waiting.namespace = NULL; + return SVN_NO_ERROR; + } + + /* We should be looking at the same tag that opened the current state. + + Unknown elements are simply skipped, so we wouldn't reach this check. + + Known elements push a new state for a given tag. Some other elemname + would imply closing an ancestor tag (where did ours go?) or a spurious + tag closure. */ + if (strcmp(elemname.name, xes->tag.name) != 0 + || strcmp(elemname.namespace, xes->tag.namespace) != 0) + return svn_error_create(SVN_ERR_XML_MALFORMED, NULL, + _("The response contains invalid XML")); + + if (xes->custom_close) + { + const svn_string_t *cdata; + + if (xes->cdata) + { + cdata = svn_stringbuf__morph_into_string(xes->cdata); +#ifdef SVN_DEBUG + /* We might toss the pool holding this structure, but it could also + be within a parent pool. In any case, for safety's sake, disable + the stringbuf against future Badness. */ + xes->cdata->pool = NULL; +#endif + } + else + cdata = NULL; + + START_CALLBACK(xmlctx); + SVN_ERR(xmlctx->closed_cb(xes, xmlctx->baton, xes->state, + cdata, xes->attrs, + xmlctx->scratch_pool)); + END_CALLBACK(xmlctx); + svn_pool_clear(xmlctx->scratch_pool); + } + + /* Pop the state. */ + xmlctx->current = xes->prev; + + /* ### not everything should go on the free state list. XES may go + ### away with the state pool. */ + xes->prev = xmlctx->free_states; + xmlctx->free_states = xes; + + /* If there is a STATE_POOL, then toss it. This will get rid of as much + memory as possible. Potentially the XES (if we didn't create a pool + right away, then XES may be in a parent pool). */ + if (xes->state_pool) + svn_pool_destroy(xes->state_pool); + + return SVN_NO_ERROR; +} + + +svn_error_t * +svn_ra_serf__xml_cb_cdata(svn_ra_serf__xml_context_t *xmlctx, + const char *data, + apr_size_t len) +{ + /* If we are waiting for a closing tag, then we are uninterested in + the cdata. Just return. */ + if (xmlctx->waiting.namespace != NULL) + return SVN_NO_ERROR; + + /* If the current state is collecting cdata, then copy the cdata. */ + if (xmlctx->current->cdata != NULL) + { + svn_stringbuf_appendbytes(xmlctx->current->cdata, data, len); + } + /* ... else if a CDATA_CB has been supplied, then invoke it for + all states. */ + else if (xmlctx->cdata_cb != NULL) + { + START_CALLBACK(xmlctx); + SVN_ERR(xmlctx->cdata_cb(xmlctx->current, + xmlctx->baton, + xmlctx->current->state, + data, len, + xmlctx->scratch_pool)); + END_CALLBACK(xmlctx); + svn_pool_clear(xmlctx->scratch_pool); + } + + return SVN_NO_ERROR; +} + |