serf.c revision 253734
1/*
2 * serf.c :  entry point for ra_serf
3 *
4 * ====================================================================
5 *    Licensed to the Apache Software Foundation (ASF) under one
6 *    or more contributor license agreements.  See the NOTICE file
7 *    distributed with this work for additional information
8 *    regarding copyright ownership.  The ASF licenses this file
9 *    to you under the Apache License, Version 2.0 (the
10 *    "License"); you may not use this file except in compliance
11 *    with the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 *    Unless required by applicable law or agreed to in writing,
16 *    software distributed under the License is distributed on an
17 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 *    KIND, either express or implied.  See the License for the
19 *    specific language governing permissions and limitations
20 *    under the License.
21 * ====================================================================
22 */
23
24
25
26#define APR_WANT_STRFUNC
27#include <apr_want.h>
28
29#include <apr_uri.h>
30#include <serf.h>
31
32#include "svn_pools.h"
33#include "svn_ra.h"
34#include "svn_dav.h"
35#include "svn_xml.h"
36#include "../libsvn_ra/ra_loader.h"
37#include "svn_config.h"
38#include "svn_delta.h"
39#include "svn_dirent_uri.h"
40#include "svn_hash.h"
41#include "svn_path.h"
42#include "svn_time.h"
43#include "svn_version.h"
44
45#include "private/svn_dav_protocol.h"
46#include "private/svn_dep_compat.h"
47#include "private/svn_fspath.h"
48#include "private/svn_subr_private.h"
49#include "svn_private_config.h"
50
51#include "ra_serf.h"
52
53
54/* Implements svn_ra__vtable_t.get_version(). */
55static const svn_version_t *
56ra_serf_version(void)
57{
58  SVN_VERSION_BODY;
59}
60
61#define RA_SERF_DESCRIPTION \
62    N_("Module for accessing a repository via WebDAV protocol using serf.")
63
64/* Implements svn_ra__vtable_t.get_description(). */
65static const char *
66ra_serf_get_description(void)
67{
68  return _(RA_SERF_DESCRIPTION);
69}
70
71/* Implements svn_ra__vtable_t.get_schemes(). */
72static const char * const *
73ra_serf_get_schemes(apr_pool_t *pool)
74{
75  static const char *serf_ssl[] = { "http", "https", NULL };
76#if 0
77  /* ### Temporary: to shut up a warning. */
78  static const char *serf_no_ssl[] = { "http", NULL };
79#endif
80
81  /* TODO: Runtime detection. */
82  return serf_ssl;
83}
84
85/* Load the setting http-auth-types from the global or server specific
86   section, parse its value and set the types of authentication we should
87   accept from the server. */
88static svn_error_t *
89load_http_auth_types(apr_pool_t *pool, svn_config_t *config,
90                     const char *server_group,
91                     int *authn_types)
92{
93  const char *http_auth_types = NULL;
94  *authn_types = SERF_AUTHN_NONE;
95
96  svn_config_get(config, &http_auth_types, SVN_CONFIG_SECTION_GLOBAL,
97               SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, NULL);
98
99  if (server_group)
100    {
101      svn_config_get(config, &http_auth_types, server_group,
102                     SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, http_auth_types);
103    }
104
105  if (http_auth_types)
106    {
107      char *token;
108      char *auth_types_list = apr_palloc(pool, strlen(http_auth_types) + 1);
109      apr_collapse_spaces(auth_types_list, http_auth_types);
110      while ((token = svn_cstring_tokenize(";", &auth_types_list)) != NULL)
111        {
112          if (svn_cstring_casecmp("basic", token) == 0)
113            *authn_types |= SERF_AUTHN_BASIC;
114          else if (svn_cstring_casecmp("digest", token) == 0)
115            *authn_types |= SERF_AUTHN_DIGEST;
116          else if (svn_cstring_casecmp("ntlm", token) == 0)
117            *authn_types |= SERF_AUTHN_NTLM;
118          else if (svn_cstring_casecmp("negotiate", token) == 0)
119            *authn_types |= SERF_AUTHN_NEGOTIATE;
120          else
121            return svn_error_createf(SVN_ERR_BAD_CONFIG_VALUE, NULL,
122                                     _("Invalid config: unknown %s "
123                                       "'%s'"),
124                                     SVN_CONFIG_OPTION_HTTP_AUTH_TYPES, token);
125      }
126    }
127  else
128    {
129      /* Nothing specified by the user, so accept all types. */
130      *authn_types = SERF_AUTHN_ALL;
131    }
132
133  return SVN_NO_ERROR;
134}
135
136/* Default HTTP timeout (in seconds); overridden by the 'http-timeout'
137   runtime configuration variable. */
138#define DEFAULT_HTTP_TIMEOUT 600
139
140/* Private symbol for the 1.9-public SVN_CONFIG_OPTION_HTTP_CHUNKED_REQUESTS */
141#define OPTION_HTTP_CHUNKED_REQUESTS "http-chunked-requests"
142
143
144static svn_error_t *
145load_config(svn_ra_serf__session_t *session,
146            apr_hash_t *config_hash,
147            apr_pool_t *pool)
148{
149  svn_config_t *config, *config_client;
150  const char *server_group;
151  const char *proxy_host = NULL;
152  const char *port_str = NULL;
153  const char *timeout_str = NULL;
154  const char *exceptions;
155  apr_port_t proxy_port;
156  svn_tristate_t chunked_requests;
157
158  if (config_hash)
159    {
160      config = svn_hash_gets(config_hash, SVN_CONFIG_CATEGORY_SERVERS);
161      config_client = svn_hash_gets(config_hash, SVN_CONFIG_CATEGORY_CONFIG);
162    }
163  else
164    {
165      config = NULL;
166      config_client = NULL;
167    }
168
169  SVN_ERR(svn_config_get_bool(config, &session->using_compression,
170                              SVN_CONFIG_SECTION_GLOBAL,
171                              SVN_CONFIG_OPTION_HTTP_COMPRESSION, TRUE));
172  svn_config_get(config, &timeout_str, SVN_CONFIG_SECTION_GLOBAL,
173                 SVN_CONFIG_OPTION_HTTP_TIMEOUT, NULL);
174
175  if (session->wc_callbacks->auth_baton)
176    {
177      if (config_client)
178        {
179          svn_auth_set_parameter(session->wc_callbacks->auth_baton,
180                                 SVN_AUTH_PARAM_CONFIG_CATEGORY_CONFIG,
181                                 config_client);
182        }
183      if (config)
184        {
185          svn_auth_set_parameter(session->wc_callbacks->auth_baton,
186                                 SVN_AUTH_PARAM_CONFIG_CATEGORY_SERVERS,
187                                 config);
188        }
189    }
190
191  /* Use the default proxy-specific settings if and only if
192     "http-proxy-exceptions" is not set to exclude this host. */
193  svn_config_get(config, &exceptions, SVN_CONFIG_SECTION_GLOBAL,
194                 SVN_CONFIG_OPTION_HTTP_PROXY_EXCEPTIONS, "");
195  if (! svn_cstring_match_glob_list(session->session_url.hostname,
196                                    svn_cstring_split(exceptions, ",",
197                                                      TRUE, pool)))
198    {
199      svn_config_get(config, &proxy_host, SVN_CONFIG_SECTION_GLOBAL,
200                     SVN_CONFIG_OPTION_HTTP_PROXY_HOST, NULL);
201      svn_config_get(config, &port_str, SVN_CONFIG_SECTION_GLOBAL,
202                     SVN_CONFIG_OPTION_HTTP_PROXY_PORT, NULL);
203      svn_config_get(config, &session->proxy_username,
204                     SVN_CONFIG_SECTION_GLOBAL,
205                     SVN_CONFIG_OPTION_HTTP_PROXY_USERNAME, NULL);
206      svn_config_get(config, &session->proxy_password,
207                     SVN_CONFIG_SECTION_GLOBAL,
208                     SVN_CONFIG_OPTION_HTTP_PROXY_PASSWORD, NULL);
209    }
210
211  /* Load the global ssl settings, if set. */
212  SVN_ERR(svn_config_get_bool(config, &session->trust_default_ca,
213                              SVN_CONFIG_SECTION_GLOBAL,
214                              SVN_CONFIG_OPTION_SSL_TRUST_DEFAULT_CA,
215                              TRUE));
216  svn_config_get(config, &session->ssl_authorities, SVN_CONFIG_SECTION_GLOBAL,
217                 SVN_CONFIG_OPTION_SSL_AUTHORITY_FILES, NULL);
218
219  /* If set, read the flag that tells us to do bulk updates or not. Defaults
220     to skelta updates. */
221  SVN_ERR(svn_config_get_tristate(config, &session->bulk_updates,
222                                  SVN_CONFIG_SECTION_GLOBAL,
223                                  SVN_CONFIG_OPTION_HTTP_BULK_UPDATES,
224                                  "auto",
225                                  svn_tristate_unknown));
226
227  /* Load the maximum number of parallel session connections. */
228  SVN_ERR(svn_config_get_int64(config, &session->max_connections,
229                               SVN_CONFIG_SECTION_GLOBAL,
230                               SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS,
231                               SVN_CONFIG_DEFAULT_OPTION_HTTP_MAX_CONNECTIONS));
232
233  /* Should we use chunked transfer encoding. */
234  SVN_ERR(svn_config_get_tristate(config, &chunked_requests,
235                                  SVN_CONFIG_SECTION_GLOBAL,
236                                  OPTION_HTTP_CHUNKED_REQUESTS,
237                                  "auto", svn_tristate_unknown));
238
239  if (config)
240    server_group = svn_config_find_group(config,
241                                         session->session_url.hostname,
242                                         SVN_CONFIG_SECTION_GROUPS, pool);
243  else
244    server_group = NULL;
245
246  if (server_group)
247    {
248      SVN_ERR(svn_config_get_bool(config, &session->using_compression,
249                                  server_group,
250                                  SVN_CONFIG_OPTION_HTTP_COMPRESSION,
251                                  session->using_compression));
252      svn_config_get(config, &timeout_str, server_group,
253                     SVN_CONFIG_OPTION_HTTP_TIMEOUT, timeout_str);
254
255      svn_auth_set_parameter(session->wc_callbacks->auth_baton,
256                             SVN_AUTH_PARAM_SERVER_GROUP, server_group);
257
258      /* Load the group proxy server settings, overriding global
259         settings.  We intentionally ignore 'http-proxy-exceptions'
260         here because, well, if this site was an exception, why is
261         there a per-server proxy configuration for it?  */
262      svn_config_get(config, &proxy_host, server_group,
263                     SVN_CONFIG_OPTION_HTTP_PROXY_HOST, proxy_host);
264      svn_config_get(config, &port_str, server_group,
265                     SVN_CONFIG_OPTION_HTTP_PROXY_PORT, port_str);
266      svn_config_get(config, &session->proxy_username, server_group,
267                     SVN_CONFIG_OPTION_HTTP_PROXY_USERNAME,
268                     session->proxy_username);
269      svn_config_get(config, &session->proxy_password, server_group,
270                     SVN_CONFIG_OPTION_HTTP_PROXY_PASSWORD,
271                     session->proxy_password);
272
273      /* Load the group ssl settings. */
274      SVN_ERR(svn_config_get_bool(config, &session->trust_default_ca,
275                                  server_group,
276                                  SVN_CONFIG_OPTION_SSL_TRUST_DEFAULT_CA,
277                                  session->trust_default_ca));
278      svn_config_get(config, &session->ssl_authorities, server_group,
279                     SVN_CONFIG_OPTION_SSL_AUTHORITY_FILES,
280                     session->ssl_authorities);
281
282      /* Load the group bulk updates flag. */
283      SVN_ERR(svn_config_get_tristate(config, &session->bulk_updates,
284                                      server_group,
285                                      SVN_CONFIG_OPTION_HTTP_BULK_UPDATES,
286                                      "auto",
287                                      session->bulk_updates));
288
289      /* Load the maximum number of parallel session connections,
290         overriding global values. */
291      SVN_ERR(svn_config_get_int64(config, &session->max_connections,
292                                   server_group,
293                                   SVN_CONFIG_OPTION_HTTP_MAX_CONNECTIONS,
294                                   session->max_connections));
295
296      /* Should we use chunked transfer encoding. */
297      SVN_ERR(svn_config_get_tristate(config, &chunked_requests,
298                                      server_group,
299                                      OPTION_HTTP_CHUNKED_REQUESTS,
300                                      "auto", chunked_requests));
301    }
302
303  /* Don't allow the http-max-connections value to be larger than our
304     compiled-in limit, or to be too small to operate.  Broken
305     functionality and angry administrators are equally undesirable. */
306  if (session->max_connections > SVN_RA_SERF__MAX_CONNECTIONS_LIMIT)
307    session->max_connections = SVN_RA_SERF__MAX_CONNECTIONS_LIMIT;
308  if (session->max_connections < 2)
309    session->max_connections = 2;
310
311  /* Parse the connection timeout value, if any. */
312  session->timeout = apr_time_from_sec(DEFAULT_HTTP_TIMEOUT);
313  if (timeout_str)
314    {
315      char *endstr;
316      const long int timeout = strtol(timeout_str, &endstr, 10);
317
318      if (*endstr)
319        return svn_error_create(SVN_ERR_BAD_CONFIG_VALUE, NULL,
320                                _("Invalid config: illegal character in "
321                                  "timeout value"));
322      if (timeout < 0)
323        return svn_error_create(SVN_ERR_BAD_CONFIG_VALUE, NULL,
324                                _("Invalid config: negative timeout value"));
325      session->timeout = apr_time_from_sec(timeout);
326    }
327  SVN_ERR_ASSERT(session->timeout >= 0);
328
329  /* Convert the proxy port value, if any. */
330  if (port_str)
331    {
332      char *endstr;
333      const long int port = strtol(port_str, &endstr, 10);
334
335      if (*endstr)
336        return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL,
337                                _("Invalid URL: illegal character in proxy "
338                                  "port number"));
339      if (port < 0)
340        return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL,
341                                _("Invalid URL: negative proxy port number"));
342      if (port > 65535)
343        return svn_error_create(SVN_ERR_RA_ILLEGAL_URL, NULL,
344                                _("Invalid URL: proxy port number greater "
345                                  "than maximum TCP port number 65535"));
346      proxy_port = (apr_port_t) port;
347    }
348  else
349    {
350      proxy_port = 80;
351    }
352
353  if (proxy_host)
354    {
355      apr_sockaddr_t *proxy_addr;
356      apr_status_t status;
357
358      status = apr_sockaddr_info_get(&proxy_addr, proxy_host,
359                                     APR_UNSPEC, proxy_port, 0,
360                                     session->pool);
361      if (status)
362        {
363          return svn_ra_serf__wrap_err(
364                   status, _("Could not resolve proxy server '%s'"),
365                   proxy_host);
366        }
367      session->using_proxy = TRUE;
368      serf_config_proxy(session->context, proxy_addr);
369    }
370  else
371    {
372      session->using_proxy = FALSE;
373    }
374
375  /* Setup detect_chunking and using_chunked_requests based on
376   * the chunked_requests tristate */
377  if (chunked_requests == svn_tristate_unknown)
378    {
379      session->detect_chunking = TRUE;
380      session->using_chunked_requests = TRUE;
381    }
382  else if (chunked_requests == svn_tristate_true)
383    {
384      session->detect_chunking = FALSE;
385      session->using_chunked_requests = TRUE;
386    }
387  else /* chunked_requests == svn_tristate_false */
388    {
389      session->detect_chunking = FALSE;
390      session->using_chunked_requests = FALSE;
391    }
392
393  /* Setup authentication. */
394  SVN_ERR(load_http_auth_types(pool, config, server_group,
395                               &session->authn_types));
396  serf_config_authn_types(session->context, session->authn_types);
397  serf_config_credentials_callback(session->context,
398                                   svn_ra_serf__credentials_callback);
399
400  return SVN_NO_ERROR;
401}
402#undef DEFAULT_HTTP_TIMEOUT
403
404static void
405svn_ra_serf__progress(void *progress_baton, apr_off_t read, apr_off_t written)
406{
407  const svn_ra_serf__session_t *serf_sess = progress_baton;
408  if (serf_sess->progress_func)
409    {
410      serf_sess->progress_func(read + written, -1,
411                               serf_sess->progress_baton,
412                               serf_sess->pool);
413    }
414}
415
416/* Implements svn_ra__vtable_t.open_session(). */
417static svn_error_t *
418svn_ra_serf__open(svn_ra_session_t *session,
419                  const char **corrected_url,
420                  const char *session_URL,
421                  const svn_ra_callbacks2_t *callbacks,
422                  void *callback_baton,
423                  apr_hash_t *config,
424                  apr_pool_t *pool)
425{
426  apr_status_t status;
427  svn_ra_serf__session_t *serf_sess;
428  apr_uri_t url;
429  const char *client_string = NULL;
430  svn_error_t *err;
431
432  if (corrected_url)
433    *corrected_url = NULL;
434
435  serf_sess = apr_pcalloc(pool, sizeof(*serf_sess));
436  serf_sess->pool = svn_pool_create(pool);
437  serf_sess->wc_callbacks = callbacks;
438  serf_sess->wc_callback_baton = callback_baton;
439  serf_sess->progress_func = callbacks->progress_func;
440  serf_sess->progress_baton = callbacks->progress_baton;
441  serf_sess->cancel_func = callbacks->cancel_func;
442  serf_sess->cancel_baton = callback_baton;
443
444  /* todo: reuse serf context across sessions */
445  serf_sess->context = serf_context_create(serf_sess->pool);
446
447  SVN_ERR(svn_ra_serf__blncache_create(&serf_sess->blncache,
448                                       serf_sess->pool));
449
450
451  status = apr_uri_parse(serf_sess->pool, session_URL, &url);
452  if (status)
453    {
454      return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
455                               _("Illegal URL '%s'"),
456                               session_URL);
457    }
458  /* Depending the version of apr-util in use, for root paths url.path
459     will be NULL or "", where serf requires "/". */
460  if (url.path == NULL || url.path[0] == '\0')
461    {
462      url.path = apr_pstrdup(serf_sess->pool, "/");
463    }
464  if (!url.port)
465    {
466      url.port = apr_uri_port_of_scheme(url.scheme);
467    }
468  serf_sess->session_url = url;
469  serf_sess->session_url_str = apr_pstrdup(serf_sess->pool, session_URL);
470  serf_sess->using_ssl = (svn_cstring_casecmp(url.scheme, "https") == 0);
471
472  serf_sess->supports_deadprop_count = svn_tristate_unknown;
473
474  serf_sess->capabilities = apr_hash_make(serf_sess->pool);
475
476  /* We have to assume that the server only supports HTTP/1.0. Once it's clear
477     HTTP/1.1 is supported, we can upgrade. */
478  serf_sess->http10 = TRUE;
479
480  /* If we switch to HTTP/1.1, then we will use chunked requests. We may disable
481     this, if we find an intervening proxy does not support chunked requests.  */
482  serf_sess->using_chunked_requests = TRUE;
483
484  SVN_ERR(load_config(serf_sess, config, serf_sess->pool));
485
486  serf_sess->conns[0] = apr_pcalloc(serf_sess->pool,
487                                    sizeof(*serf_sess->conns[0]));
488  serf_sess->conns[0]->bkt_alloc =
489          serf_bucket_allocator_create(serf_sess->pool, NULL, NULL);
490  serf_sess->conns[0]->session = serf_sess;
491  serf_sess->conns[0]->last_status_code = -1;
492
493  /* create the user agent string */
494  if (callbacks->get_client_string)
495    SVN_ERR(callbacks->get_client_string(callback_baton, &client_string, pool));
496
497  if (client_string)
498    serf_sess->useragent = apr_pstrcat(pool, USER_AGENT, " ",
499                                       client_string, (char *)NULL);
500  else
501    serf_sess->useragent = USER_AGENT;
502
503  /* go ahead and tell serf about the connection. */
504  status =
505    serf_connection_create2(&serf_sess->conns[0]->conn,
506                            serf_sess->context,
507                            url,
508                            svn_ra_serf__conn_setup, serf_sess->conns[0],
509                            svn_ra_serf__conn_closed, serf_sess->conns[0],
510                            serf_sess->pool);
511  if (status)
512    return svn_ra_serf__wrap_err(status, NULL);
513
514  /* Set the progress callback. */
515  serf_context_set_progress_cb(serf_sess->context, svn_ra_serf__progress,
516                               serf_sess);
517
518  serf_sess->num_conns = 1;
519
520  session->priv = serf_sess;
521
522  err = svn_ra_serf__exchange_capabilities(serf_sess, corrected_url, pool);
523
524  /* serf should produce a usable error code instead of APR_EGENERAL */
525  if (err && err->apr_err == APR_EGENERAL)
526    err = svn_error_createf(SVN_ERR_RA_DAV_REQUEST_FAILED, err,
527                            _("Connection to '%s' failed"), session_URL);
528  SVN_ERR(err);
529
530  /* We have set up a useful connection (that doesn't indication a redirect).
531     If we've been told there is possibly a worrisome proxy in our path to the
532     server AND we switched to HTTP/1.1 (chunked requests), then probe for
533     problems in any proxy.  */
534  if ((corrected_url == NULL || *corrected_url == NULL)
535      && serf_sess->detect_chunking && !serf_sess->http10)
536    SVN_ERR(svn_ra_serf__probe_proxy(serf_sess, pool));
537
538  return SVN_NO_ERROR;
539}
540
541/* Implements svn_ra__vtable_t.reparent(). */
542static svn_error_t *
543svn_ra_serf__reparent(svn_ra_session_t *ra_session,
544                      const char *url,
545                      apr_pool_t *pool)
546{
547  svn_ra_serf__session_t *session = ra_session->priv;
548  apr_uri_t new_url;
549  apr_status_t status;
550
551  /* If it's the URL we already have, wave our hands and do nothing. */
552  if (strcmp(session->session_url_str, url) == 0)
553    {
554      return SVN_NO_ERROR;
555    }
556
557  if (!session->repos_root_str)
558    {
559      const char *vcc_url;
560      SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool));
561    }
562
563  if (!svn_uri__is_ancestor(session->repos_root_str, url))
564    {
565      return svn_error_createf(
566          SVN_ERR_RA_ILLEGAL_URL, NULL,
567          _("URL '%s' is not a child of the session's repository root "
568            "URL '%s'"), url, session->repos_root_str);
569    }
570
571  status = apr_uri_parse(pool, url, &new_url);
572  if (status)
573    {
574      return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
575                               _("Illegal repository URL '%s'"), url);
576    }
577
578  /* Depending the version of apr-util in use, for root paths url.path
579     will be NULL or "", where serf requires "/". */
580  /* ### Maybe we should use a string buffer for these strings so we
581     ### don't allocate memory in the session on every reparent? */
582  if (new_url.path == NULL || new_url.path[0] == '\0')
583    {
584      session->session_url.path = apr_pstrdup(session->pool, "/");
585    }
586  else
587    {
588      session->session_url.path = apr_pstrdup(session->pool, new_url.path);
589    }
590  session->session_url_str = apr_pstrdup(session->pool, url);
591
592  return SVN_NO_ERROR;
593}
594
595/* Implements svn_ra__vtable_t.get_session_url(). */
596static svn_error_t *
597svn_ra_serf__get_session_url(svn_ra_session_t *ra_session,
598                             const char **url,
599                             apr_pool_t *pool)
600{
601  svn_ra_serf__session_t *session = ra_session->priv;
602  *url = apr_pstrdup(pool, session->session_url_str);
603  return SVN_NO_ERROR;
604}
605
606/* Implements svn_ra__vtable_t.get_latest_revnum(). */
607static svn_error_t *
608svn_ra_serf__get_latest_revnum(svn_ra_session_t *ra_session,
609                               svn_revnum_t *latest_revnum,
610                               apr_pool_t *pool)
611{
612  svn_ra_serf__session_t *session = ra_session->priv;
613
614  return svn_error_trace(svn_ra_serf__get_youngest_revnum(
615                           latest_revnum, session, pool));
616}
617
618/* Implements svn_ra__vtable_t.rev_proplist(). */
619static svn_error_t *
620svn_ra_serf__rev_proplist(svn_ra_session_t *ra_session,
621                          svn_revnum_t rev,
622                          apr_hash_t **ret_props,
623                          apr_pool_t *pool)
624{
625  svn_ra_serf__session_t *session = ra_session->priv;
626  apr_hash_t *props;
627  const char *propfind_path;
628
629  if (SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session))
630    {
631      propfind_path = apr_psprintf(pool, "%s/%ld", session->rev_stub, rev);
632
633      /* svn_ra_serf__retrieve_props() wants to added the revision as
634         a Label to the PROPFIND, which isn't really necessary when
635         querying a rev-stub URI.  *Shrug*  Probably okay to leave the
636         Label, but whatever. */
637      rev = SVN_INVALID_REVNUM;
638    }
639  else
640    {
641      /* Use the VCC as the propfind target path. */
642      SVN_ERR(svn_ra_serf__discover_vcc(&propfind_path, session, NULL, pool));
643    }
644
645  /* ### fix: fetch hash of *just* the PATH@REV props. no nested hash.  */
646  SVN_ERR(svn_ra_serf__retrieve_props(&props, session, session->conns[0],
647                                      propfind_path, rev, "0", all_props,
648                                      pool, pool));
649
650  SVN_ERR(svn_ra_serf__select_revprops(ret_props, propfind_path, rev, props,
651                                       pool, pool));
652
653  return SVN_NO_ERROR;
654}
655
656/* Implements svn_ra__vtable_t.rev_prop(). */
657static svn_error_t *
658svn_ra_serf__rev_prop(svn_ra_session_t *session,
659                      svn_revnum_t rev,
660                      const char *name,
661                      svn_string_t **value,
662                      apr_pool_t *pool)
663{
664  apr_hash_t *props;
665
666  SVN_ERR(svn_ra_serf__rev_proplist(session, rev, &props, pool));
667
668  *value = svn_hash_gets(props, name);
669
670  return SVN_NO_ERROR;
671}
672
673static svn_error_t *
674fetch_path_props(apr_hash_t **props,
675                 svn_ra_serf__session_t *session,
676                 const char *session_relpath,
677                 svn_revnum_t revision,
678                 const svn_ra_serf__dav_props_t *desired_props,
679                 apr_pool_t *result_pool,
680                 apr_pool_t *scratch_pool)
681{
682  const char *url;
683
684  url = session->session_url.path;
685
686  /* If we have a relative path, append it. */
687  if (session_relpath)
688    url = svn_path_url_add_component2(url, session_relpath, scratch_pool);
689
690  /* If we were given a specific revision, get a URL that refers to that
691     specific revision (rather than floating with HEAD).  */
692  if (SVN_IS_VALID_REVNUM(revision))
693    {
694      SVN_ERR(svn_ra_serf__get_stable_url(&url, NULL /* latest_revnum */,
695                                          session, NULL /* conn */,
696                                          url, revision,
697                                          scratch_pool, scratch_pool));
698    }
699
700  /* URL is stable, so we use SVN_INVALID_REVNUM since it is now irrelevant.
701     Or we started with SVN_INVALID_REVNUM and URL may be floating.  */
702  SVN_ERR(svn_ra_serf__fetch_node_props(props, session->conns[0],
703                                        url, SVN_INVALID_REVNUM,
704                                        desired_props,
705                                        result_pool, scratch_pool));
706
707  return SVN_NO_ERROR;
708}
709
710/* Implements svn_ra__vtable_t.check_path(). */
711static svn_error_t *
712svn_ra_serf__check_path(svn_ra_session_t *ra_session,
713                        const char *rel_path,
714                        svn_revnum_t revision,
715                        svn_node_kind_t *kind,
716                        apr_pool_t *pool)
717{
718  svn_ra_serf__session_t *session = ra_session->priv;
719  apr_hash_t *props;
720
721  svn_error_t *err = fetch_path_props(&props, session, rel_path,
722                                      revision, check_path_props,
723                                      pool, pool);
724
725  if (err && err->apr_err == SVN_ERR_FS_NOT_FOUND)
726    {
727      svn_error_clear(err);
728      *kind = svn_node_none;
729    }
730  else
731    {
732      /* Any other error, raise to caller. */
733      if (err)
734        return svn_error_trace(err);
735
736      SVN_ERR(svn_ra_serf__get_resource_type(kind, props));
737    }
738
739  return SVN_NO_ERROR;
740}
741
742
743struct dirent_walker_baton_t {
744  /* Update the fields in this entry.  */
745  svn_dirent_t *entry;
746
747  svn_tristate_t *supports_deadprop_count;
748
749  /* If allocations are necessary, then use this pool.  */
750  apr_pool_t *result_pool;
751};
752
753static svn_error_t *
754dirent_walker(void *baton,
755              const char *ns,
756              const char *name,
757              const svn_string_t *val,
758              apr_pool_t *scratch_pool)
759{
760  struct dirent_walker_baton_t *dwb = baton;
761
762  if (strcmp(ns, SVN_DAV_PROP_NS_CUSTOM) == 0)
763    {
764      dwb->entry->has_props = TRUE;
765    }
766  else if (strcmp(ns, SVN_DAV_PROP_NS_SVN) == 0)
767    {
768      dwb->entry->has_props = TRUE;
769    }
770  else if (strcmp(ns, SVN_DAV_PROP_NS_DAV) == 0)
771    {
772      if(strcmp(name, "deadprop-count") == 0)
773        {
774          if (*val->data)
775            {
776              apr_int64_t deadprop_count;
777              SVN_ERR(svn_cstring_atoi64(&deadprop_count, val->data));
778              dwb->entry->has_props = deadprop_count > 0;
779              if (dwb->supports_deadprop_count)
780                *dwb->supports_deadprop_count = svn_tristate_true;
781            }
782          else if (dwb->supports_deadprop_count)
783            *dwb->supports_deadprop_count = svn_tristate_false;
784        }
785    }
786  else if (strcmp(ns, "DAV:") == 0)
787    {
788      if (strcmp(name, SVN_DAV__VERSION_NAME) == 0)
789        {
790          dwb->entry->created_rev = SVN_STR_TO_REV(val->data);
791        }
792      else if (strcmp(name, "creator-displayname") == 0)
793        {
794          dwb->entry->last_author = val->data;
795        }
796      else if (strcmp(name, SVN_DAV__CREATIONDATE) == 0)
797        {
798          SVN_ERR(svn_time_from_cstring(&dwb->entry->time,
799                                        val->data,
800                                        dwb->result_pool));
801        }
802      else if (strcmp(name, "getcontentlength") == 0)
803        {
804          /* 'getcontentlength' property is empty for directories. */
805          if (val->len)
806            {
807              SVN_ERR(svn_cstring_atoi64(&dwb->entry->size, val->data));
808            }
809        }
810      else if (strcmp(name, "resourcetype") == 0)
811        {
812          if (strcmp(val->data, "collection") == 0)
813            {
814              dwb->entry->kind = svn_node_dir;
815            }
816          else
817            {
818              dwb->entry->kind = svn_node_file;
819            }
820        }
821    }
822
823  return SVN_NO_ERROR;
824}
825
826struct path_dirent_visitor_t {
827  apr_hash_t *full_paths;
828  apr_hash_t *base_paths;
829  const char *orig_path;
830  svn_tristate_t supports_deadprop_count;
831  apr_pool_t *result_pool;
832};
833
834static svn_error_t *
835path_dirent_walker(void *baton,
836                   const char *path, apr_ssize_t path_len,
837                   const char *ns, apr_ssize_t ns_len,
838                   const char *name, apr_ssize_t name_len,
839                   const svn_string_t *val,
840                   apr_pool_t *pool)
841{
842  struct path_dirent_visitor_t *dirents = baton;
843  struct dirent_walker_baton_t dwb;
844  svn_dirent_t *entry;
845
846  /* Skip our original path. */
847  if (strcmp(path, dirents->orig_path) == 0)
848    {
849      return SVN_NO_ERROR;
850    }
851
852  entry = apr_hash_get(dirents->full_paths, path, path_len);
853
854  if (!entry)
855    {
856      const char *base_name;
857
858      entry = svn_dirent_create(pool);
859
860      apr_hash_set(dirents->full_paths, path, path_len, entry);
861
862      base_name = svn_path_uri_decode(svn_urlpath__basename(path, pool),
863                                      pool);
864
865      svn_hash_sets(dirents->base_paths, base_name, entry);
866    }
867
868  dwb.entry = entry;
869  dwb.supports_deadprop_count = &dirents->supports_deadprop_count;
870  dwb.result_pool = dirents->result_pool;
871  return svn_error_trace(dirent_walker(&dwb, ns, name, val, pool));
872}
873
874static const svn_ra_serf__dav_props_t *
875get_dirent_props(apr_uint32_t dirent_fields,
876                 svn_ra_serf__session_t *session,
877                 apr_pool_t *pool)
878{
879  svn_ra_serf__dav_props_t *prop;
880  apr_array_header_t *props = apr_array_make
881    (pool, 7, sizeof(svn_ra_serf__dav_props_t));
882
883  if (session->supports_deadprop_count != svn_tristate_false
884      || ! (dirent_fields & SVN_DIRENT_HAS_PROPS))
885    {
886      if (dirent_fields & SVN_DIRENT_KIND)
887        {
888          prop = apr_array_push(props);
889          prop->namespace = "DAV:";
890          prop->name = "resourcetype";
891        }
892
893      if (dirent_fields & SVN_DIRENT_SIZE)
894        {
895          prop = apr_array_push(props);
896          prop->namespace = "DAV:";
897          prop->name = "getcontentlength";
898        }
899
900      if (dirent_fields & SVN_DIRENT_HAS_PROPS)
901        {
902          prop = apr_array_push(props);
903          prop->namespace = SVN_DAV_PROP_NS_DAV;
904          prop->name = "deadprop-count";
905        }
906
907      if (dirent_fields & SVN_DIRENT_CREATED_REV)
908        {
909          svn_ra_serf__dav_props_t *p = apr_array_push(props);
910          p->namespace = "DAV:";
911          p->name = SVN_DAV__VERSION_NAME;
912        }
913
914      if (dirent_fields & SVN_DIRENT_TIME)
915        {
916          prop = apr_array_push(props);
917          prop->namespace = "DAV:";
918          prop->name = SVN_DAV__CREATIONDATE;
919        }
920
921      if (dirent_fields & SVN_DIRENT_LAST_AUTHOR)
922        {
923          prop = apr_array_push(props);
924          prop->namespace = "DAV:";
925          prop->name = "creator-displayname";
926        }
927    }
928  else
929    {
930      /* We found an old subversion server that can't handle
931         the deadprop-count property in the way we expect.
932
933         The neon behavior is to retrieve all properties in this case */
934      prop = apr_array_push(props);
935      prop->namespace = "DAV:";
936      prop->name = "allprop";
937    }
938
939  prop = apr_array_push(props);
940  prop->namespace = NULL;
941  prop->name = NULL;
942
943  return (svn_ra_serf__dav_props_t *) props->elts;
944}
945
946/* Implements svn_ra__vtable_t.stat(). */
947static svn_error_t *
948svn_ra_serf__stat(svn_ra_session_t *ra_session,
949                  const char *rel_path,
950                  svn_revnum_t revision,
951                  svn_dirent_t **dirent,
952                  apr_pool_t *pool)
953{
954  svn_ra_serf__session_t *session = ra_session->priv;
955  apr_hash_t *props;
956  svn_error_t *err;
957  struct dirent_walker_baton_t dwb;
958  svn_tristate_t deadprop_count = svn_tristate_unknown;
959
960  err = fetch_path_props(&props,
961                         session, rel_path, revision,
962                         get_dirent_props(SVN_DIRENT_ALL, session, pool),
963                         pool, pool);
964  if (err)
965    {
966      if (err->apr_err == SVN_ERR_FS_NOT_FOUND)
967        {
968          svn_error_clear(err);
969          *dirent = NULL;
970          return SVN_NO_ERROR;
971        }
972      else
973        return svn_error_trace(err);
974    }
975
976  dwb.entry = svn_dirent_create(pool);
977  dwb.supports_deadprop_count = &deadprop_count;
978  dwb.result_pool = pool;
979  SVN_ERR(svn_ra_serf__walk_node_props(props, dirent_walker, &dwb, pool));
980
981  if (deadprop_count == svn_tristate_false
982      && session->supports_deadprop_count == svn_tristate_unknown
983      && !dwb.entry->has_props)
984    {
985      /* We have to requery as the server didn't give us the right
986         information */
987      session->supports_deadprop_count = svn_tristate_false;
988
989      SVN_ERR(fetch_path_props(&props,
990                               session, rel_path, SVN_INVALID_REVNUM,
991                               get_dirent_props(SVN_DIRENT_ALL, session, pool),
992                               pool, pool));
993
994      SVN_ERR(svn_ra_serf__walk_node_props(props, dirent_walker, &dwb, pool));
995    }
996
997  if (deadprop_count != svn_tristate_unknown)
998    session->supports_deadprop_count = deadprop_count;
999
1000  *dirent = dwb.entry;
1001
1002  return SVN_NO_ERROR;
1003}
1004
1005/* Reads the 'resourcetype' property from the list PROPS and checks if the
1006 * resource at PATH@REVISION really is a directory. Returns
1007 * SVN_ERR_FS_NOT_DIRECTORY if not.
1008 */
1009static svn_error_t *
1010resource_is_directory(apr_hash_t *props)
1011{
1012  svn_node_kind_t kind;
1013
1014  SVN_ERR(svn_ra_serf__get_resource_type(&kind, props));
1015
1016  if (kind != svn_node_dir)
1017    {
1018      return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL,
1019                              _("Can't get entries of non-directory"));
1020    }
1021
1022  return SVN_NO_ERROR;
1023}
1024
1025/* Implements svn_ra__vtable_t.get_dir(). */
1026static svn_error_t *
1027svn_ra_serf__get_dir(svn_ra_session_t *ra_session,
1028                     apr_hash_t **dirents,
1029                     svn_revnum_t *fetched_rev,
1030                     apr_hash_t **ret_props,
1031                     const char *rel_path,
1032                     svn_revnum_t revision,
1033                     apr_uint32_t dirent_fields,
1034                     apr_pool_t *pool)
1035{
1036  svn_ra_serf__session_t *session = ra_session->priv;
1037  const char *path;
1038
1039  path = session->session_url.path;
1040
1041  /* If we have a relative path, URI encode and append it. */
1042  if (rel_path)
1043    {
1044      path = svn_path_url_add_component2(path, rel_path, pool);
1045    }
1046
1047  /* If the user specified a peg revision other than HEAD, we have to fetch
1048     the baseline collection url for that revision. If not, we can use the
1049     public url. */
1050  if (SVN_IS_VALID_REVNUM(revision) || fetched_rev)
1051    {
1052      SVN_ERR(svn_ra_serf__get_stable_url(&path, fetched_rev,
1053                                          session, NULL /* conn */,
1054                                          path, revision,
1055                                          pool, pool));
1056      revision = SVN_INVALID_REVNUM;
1057    }
1058  /* REVISION is always SVN_INVALID_REVNUM  */
1059  SVN_ERR_ASSERT(!SVN_IS_VALID_REVNUM(revision));
1060
1061  /* If we're asked for children, fetch them now. */
1062  if (dirents)
1063    {
1064      struct path_dirent_visitor_t dirent_walk;
1065      apr_hash_t *props;
1066      const char *rtype;
1067
1068      /* Always request node kind to check that path is really a
1069       * directory.
1070       */
1071      dirent_fields |= SVN_DIRENT_KIND;
1072      SVN_ERR(svn_ra_serf__retrieve_props(&props, session, session->conns[0],
1073                                          path, SVN_INVALID_REVNUM, "1",
1074                                          get_dirent_props(dirent_fields,
1075                                                           session, pool),
1076                                          pool, pool));
1077
1078      /* Check if the path is really a directory. */
1079      rtype = svn_ra_serf__get_prop(props, path, "DAV:", "resourcetype");
1080      if (rtype == NULL || strcmp(rtype, "collection") != 0)
1081        return svn_error_create(SVN_ERR_FS_NOT_DIRECTORY, NULL,
1082                                _("Can't get entries of non-directory"));
1083
1084      /* We're going to create two hashes to help the walker along.
1085       * We're going to return the 2nd one back to the caller as it
1086       * will have the basenames it expects.
1087       */
1088      dirent_walk.full_paths = apr_hash_make(pool);
1089      dirent_walk.base_paths = apr_hash_make(pool);
1090      dirent_walk.orig_path = svn_urlpath__canonicalize(path, pool);
1091      dirent_walk.supports_deadprop_count = svn_tristate_unknown;
1092      dirent_walk.result_pool = pool;
1093
1094      SVN_ERR(svn_ra_serf__walk_all_paths(props, SVN_INVALID_REVNUM,
1095                                          path_dirent_walker, &dirent_walk,
1096                                          pool));
1097
1098      if (dirent_walk.supports_deadprop_count == svn_tristate_false
1099          && session->supports_deadprop_count == svn_tristate_unknown
1100          && dirent_fields & SVN_DIRENT_HAS_PROPS)
1101        {
1102          /* We have to requery as the server didn't give us the right
1103             information */
1104          session->supports_deadprop_count = svn_tristate_false;
1105          SVN_ERR(svn_ra_serf__retrieve_props(&props, session,
1106                                              session->conns[0],
1107                                              path, SVN_INVALID_REVNUM, "1",
1108                                              get_dirent_props(dirent_fields,
1109                                                               session, pool),
1110                                              pool, pool));
1111
1112          apr_hash_clear(dirent_walk.full_paths);
1113          apr_hash_clear(dirent_walk.base_paths);
1114
1115          SVN_ERR(svn_ra_serf__walk_all_paths(props, SVN_INVALID_REVNUM,
1116                                              path_dirent_walker,
1117                                              &dirent_walk, pool));
1118        }
1119
1120      *dirents = dirent_walk.base_paths;
1121
1122      if (dirent_walk.supports_deadprop_count != svn_tristate_unknown)
1123        session->supports_deadprop_count = dirent_walk.supports_deadprop_count;
1124    }
1125
1126  /* If we're asked for the directory properties, fetch them too. */
1127  if (ret_props)
1128    {
1129      apr_hash_t *props;
1130
1131      SVN_ERR(svn_ra_serf__fetch_node_props(&props, session->conns[0],
1132                                            path, SVN_INVALID_REVNUM,
1133                                            all_props,
1134                                            pool, pool));
1135
1136      /* Check if the path is really a directory. */
1137      SVN_ERR(resource_is_directory(props));
1138
1139      /* ### flatten_props() does not copy PROPVALUE, but fetch_node_props()
1140         ### put them into POOL, so we're okay.  */
1141      SVN_ERR(svn_ra_serf__flatten_props(ret_props, props, pool, pool));
1142    }
1143
1144  return SVN_NO_ERROR;
1145}
1146
1147svn_error_t *
1148svn_ra_serf__get_repos_root(svn_ra_session_t *ra_session,
1149                            const char **url,
1150                            apr_pool_t *pool)
1151{
1152  svn_ra_serf__session_t *session = ra_session->priv;
1153
1154  if (!session->repos_root_str)
1155    {
1156      const char *vcc_url;
1157      SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool));
1158    }
1159
1160  *url = session->repos_root_str;
1161  return SVN_NO_ERROR;
1162}
1163
1164/* TODO: to fetch the uuid from the repository, we need:
1165   1. a path that exists in HEAD
1166   2. a path that's readable
1167
1168   get_uuid handles the case where a path doesn't exist in HEAD and also the
1169   case where the root of the repository is not readable.
1170   However, it does not handle the case where we're fetching path not existing
1171   in HEAD of a repository with unreadable root directory.
1172
1173   Implements svn_ra__vtable_t.get_uuid().
1174 */
1175static svn_error_t *
1176svn_ra_serf__get_uuid(svn_ra_session_t *ra_session,
1177                      const char **uuid,
1178                      apr_pool_t *pool)
1179{
1180  svn_ra_serf__session_t *session = ra_session->priv;
1181
1182  if (!session->uuid)
1183    {
1184      const char *vcc_url;
1185
1186      /* We should never get here if we have HTTP v2 support, because
1187         any server with that support should be transmitting the
1188         UUID in the initial OPTIONS response.  */
1189      SVN_ERR_ASSERT(! SVN_RA_SERF__HAVE_HTTPV2_SUPPORT(session));
1190
1191      /* We're not interested in vcc_url and relative_url, but this call also
1192         stores the repository's uuid in the session. */
1193      SVN_ERR(svn_ra_serf__discover_vcc(&vcc_url, session, NULL, pool));
1194      if (!session->uuid)
1195        {
1196          return svn_error_create(SVN_ERR_RA_DAV_RESPONSE_HEADER_BADNESS, NULL,
1197                                  _("The UUID property was not found on the "
1198                                    "resource or any of its parents"));
1199        }
1200    }
1201
1202  *uuid = session->uuid;
1203
1204  return SVN_NO_ERROR;
1205}
1206
1207
1208static const svn_ra__vtable_t serf_vtable = {
1209  ra_serf_version,
1210  ra_serf_get_description,
1211  ra_serf_get_schemes,
1212  svn_ra_serf__open,
1213  svn_ra_serf__reparent,
1214  svn_ra_serf__get_session_url,
1215  svn_ra_serf__get_latest_revnum,
1216  svn_ra_serf__get_dated_revision,
1217  svn_ra_serf__change_rev_prop,
1218  svn_ra_serf__rev_proplist,
1219  svn_ra_serf__rev_prop,
1220  svn_ra_serf__get_commit_editor,
1221  svn_ra_serf__get_file,
1222  svn_ra_serf__get_dir,
1223  svn_ra_serf__get_mergeinfo,
1224  svn_ra_serf__do_update,
1225  svn_ra_serf__do_switch,
1226  svn_ra_serf__do_status,
1227  svn_ra_serf__do_diff,
1228  svn_ra_serf__get_log,
1229  svn_ra_serf__check_path,
1230  svn_ra_serf__stat,
1231  svn_ra_serf__get_uuid,
1232  svn_ra_serf__get_repos_root,
1233  svn_ra_serf__get_locations,
1234  svn_ra_serf__get_location_segments,
1235  svn_ra_serf__get_file_revs,
1236  svn_ra_serf__lock,
1237  svn_ra_serf__unlock,
1238  svn_ra_serf__get_lock,
1239  svn_ra_serf__get_locks,
1240  svn_ra_serf__replay,
1241  svn_ra_serf__has_capability,
1242  svn_ra_serf__replay_range,
1243  svn_ra_serf__get_deleted_rev,
1244  svn_ra_serf__register_editor_shim_callbacks,
1245  svn_ra_serf__get_inherited_props
1246};
1247
1248svn_error_t *
1249svn_ra_serf__init(const svn_version_t *loader_version,
1250                  const svn_ra__vtable_t **vtable,
1251                  apr_pool_t *pool)
1252{
1253  static const svn_version_checklist_t checklist[] =
1254    {
1255      { "svn_subr",  svn_subr_version },
1256      { "svn_delta", svn_delta_version },
1257      { NULL, NULL }
1258    };
1259  int serf_major;
1260  int serf_minor;
1261  int serf_patch;
1262
1263  SVN_ERR(svn_ver_check_list(ra_serf_version(), checklist));
1264
1265  /* Simplified version check to make sure we can safely use the
1266     VTABLE parameter. The RA loader does a more exhaustive check. */
1267  if (loader_version->major != SVN_VER_MAJOR)
1268    {
1269      return svn_error_createf(
1270         SVN_ERR_VERSION_MISMATCH, NULL,
1271         _("Unsupported RA loader version (%d) for ra_serf"),
1272         loader_version->major);
1273    }
1274
1275  /* Make sure that we have loaded a compatible library: the MAJOR must
1276     match, and the minor must be at *least* what we compiled against.
1277     The patch level is simply ignored.  */
1278  serf_lib_version(&serf_major, &serf_minor, &serf_patch);
1279  if (serf_major != SERF_MAJOR_VERSION
1280      || serf_minor < SERF_MINOR_VERSION)
1281    {
1282      return svn_error_createf(
1283         /* ### should return a unique error  */
1284         SVN_ERR_VERSION_MISMATCH, NULL,
1285         _("ra_serf was compiled for serf %d.%d.%d but loaded "
1286           "an incompatible %d.%d.%d library"),
1287         SERF_MAJOR_VERSION, SERF_MINOR_VERSION, SERF_PATCH_VERSION,
1288         serf_major, serf_minor, serf_patch);
1289    }
1290
1291  *vtable = &serf_vtable;
1292
1293  return SVN_NO_ERROR;
1294}
1295
1296/* Compatibility wrapper for pre-1.2 subversions.  Needed? */
1297#define NAME "ra_serf"
1298#define DESCRIPTION RA_SERF_DESCRIPTION
1299#define VTBL serf_vtable
1300#define INITFUNC svn_ra_serf__init
1301#define COMPAT_INITFUNC svn_ra_serf_init
1302#include "../libsvn_ra/wrapper_template.h"
1303