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