serve.c   [plain text]


/*
 * serve.c :  Functions for serving the Subversion protocol
 *
 * ====================================================================
 * Copyright (c) 2000-2008 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */




#include <limits.h> /* for UINT_MAX */
#include <stdarg.h>

#define APR_WANT_STRFUNC
#include <apr_want.h>
#include <apr_general.h>
#include <apr_lib.h>
#include <apr_strings.h>

#include "svn_compat.h"
#include "svn_private_config.h"  /* For SVN_PATH_LOCAL_SEPARATOR */
#include "svn_types.h"
#include "svn_string.h"
#include "svn_pools.h"
#include "svn_error.h"
#include "svn_ra.h"              /* for SVN_RA_CAPABILITY_* */
#include "svn_ra_svn.h"
#include "svn_repos.h"
#include "svn_path.h"
#include "svn_time.h"
#include "svn_config.h"
#include "svn_props.h"
#include "svn_mergeinfo.h"
#include "svn_user.h"

#include "private/svn_log.h"
#include "private/svn_mergeinfo_private.h"

#ifdef HAVE_UNISTD_H
#include <unistd.h>   /* For getpid() */
#endif

#include "server.h"

typedef struct {
  apr_pool_t *pool;
  svn_revnum_t *new_rev;
  const char **date;
  const char **author;
  const char **post_commit_err;
} commit_callback_baton_t;

typedef struct {
  server_baton_t *sb;
  const char *repos_url;  /* Decoded repository URL. */
  void *report_baton;
  svn_error_t *err;
  /* so update() can distinguish checkout from update in logging */
  int entry_counter;
  svn_boolean_t only_empty_entries;
  /* for diff() logging */
  svn_revnum_t *from_rev;
} report_driver_baton_t;

typedef struct {
  const char *fs_path;
  svn_ra_svn_conn_t *conn;
  int stack_depth;
} log_baton_t;

typedef struct {
  svn_ra_svn_conn_t *conn;
  apr_pool_t *pool;  /* Pool provided in the handler call. */
} file_revs_baton_t;

typedef struct {
  server_baton_t *server;
  svn_ra_svn_conn_t *conn;
  apr_pool_t *pool;
} fs_warning_baton_t;


/* Write LEN bytes of ERRSTR to LOG_FILE with svn_io_file_write(). */
static svn_error_t *
log_write(apr_file_t *log_file, const char *errstr, apr_size_t len,
          apr_pool_t *pool)
{
  return svn_io_file_write(log_file, errstr, &len, pool);
}

void
log_error(svn_error_t *err, apr_file_t *log_file, const char *remote_host,
          const char *user, const char *repos, apr_pool_t *pool)
{
  const char *timestr, *continuation;
  char errbuf[256];
  /* 8192 from MAX_STRING_LEN in from httpd-2.2.4/include/httpd.h */
  char errstr[8192];

  if (err == SVN_NO_ERROR)
    return;

  if (log_file == NULL)
    return;

  timestr = svn_time_to_cstring(apr_time_now(), pool);
  remote_host = (remote_host ? remote_host : "-");
  user = (user ? user : "-");
  repos = (repos ? repos : "-");

  continuation = "";
  while (err != NULL)
    {
      const char *message = svn_err_best_message(err, errbuf, sizeof(errbuf));
      /* based on httpd-2.2.4/server/log.c:log_error_core */
      apr_size_t len = apr_snprintf(errstr, sizeof(errstr),
                                    "%" APR_PID_T_FMT
                                    " %s %s %s %s ERR%s %s %ld %d ",
                                    getpid(), timestr, remote_host, user,
                                    repos, continuation,
                                    err->file ? err->file : "-", err->line,
                                    err->apr_err);

      len += escape_errorlog_item(errstr + len, message,
                                  sizeof(errstr) - len);
      /* Truncate for the terminator (as apr_snprintf does) */
      if (len > sizeof(errstr) - sizeof(APR_EOL_STR)) {
        len = sizeof(errstr) - sizeof(APR_EOL_STR);
      }
      strcpy(errstr + len, APR_EOL_STR);
      len += strlen(APR_EOL_STR);
      svn_error_clear(log_write(log_file, errstr, len, pool));

      continuation = "-";
      err = err->child;
    }
}

/* Call log_error with log_file, remote_host, user, and repos
   arguments from SERVER and CONN. */
static void
log_server_error(svn_error_t *err, server_baton_t *server,
                 svn_ra_svn_conn_t *conn, apr_pool_t *pool)
{
  log_error(err, server->log_file, svn_ra_svn_conn_remote_host(conn),
            server->user, server->repos_name, pool);
}

/* svn_error_create() a new error, log_server_error() it, and
   return it. */
static svn_error_t *
error_create_and_log(apr_status_t apr_err, svn_error_t *child,
                     const char *message, server_baton_t *server,
                     svn_ra_svn_conn_t *conn, apr_pool_t *pool)
{
  svn_error_t *err = svn_error_create(apr_err, child, message);
  log_server_error(err, server, conn, pool);
  return err;
}

/* Log a client command. */
static svn_error_t *log_command(server_baton_t *b,
                                svn_ra_svn_conn_t *conn,
                                apr_pool_t *pool,
                                const char *fmt, ...)
{
  const char *remote_host, *timestr, *log, *line;
  va_list ap;
  apr_size_t nbytes;

  if (b->log_file == NULL)
    return SVN_NO_ERROR;

  remote_host = svn_ra_svn_conn_remote_host(conn);
  timestr = svn_time_to_cstring(apr_time_now(), pool);

  va_start(ap, fmt);
  log = apr_pvsprintf(pool, fmt, ap);
  va_end(ap);

  line = apr_psprintf(pool, "%" APR_PID_T_FMT
                      " %s %s %s %s %s" APR_EOL_STR,
                      getpid(), timestr,
                      (remote_host ? remote_host : "-"),
                      (b->user ? b->user : "-"), b->repos_name, log);
  nbytes = strlen(line);

  return log_write(b->log_file, line, nbytes, pool);
}

svn_error_t *load_configs(svn_config_t **cfg,
                          svn_config_t **pwdb,
                          svn_authz_t **authzdb,
                          const char *filename,
                          svn_boolean_t must_exist,
                          const char *base,
                          server_baton_t *server,
                          svn_ra_svn_conn_t *conn,
                          apr_pool_t *pool)
{
  const char *pwdb_path, *authzdb_path;
  svn_error_t *err;

  SVN_ERR(svn_config_read(cfg, filename, must_exist, pool));

  svn_config_get(*cfg, &pwdb_path, SVN_CONFIG_SECTION_GENERAL,
                 SVN_CONFIG_OPTION_PASSWORD_DB, NULL);

  *pwdb = NULL;
  if (pwdb_path)
    {
      pwdb_path = svn_path_join(base, pwdb_path, pool);

      err = svn_config_read(pwdb, pwdb_path, TRUE, pool);
      if (err)
        {
          if (server)
            /* Called by listening server; log error no matter what it is. */
            log_server_error(err, server, conn, pool);

          /* Because it may be possible to read the pwdb file with some
             access methods and not others, ignore errors reading the pwdb
             file and just don't present password authentication as an
             option.  Also, some authentications (e.g. --tunnel) can
             proceed without it anyway.

             ### Not entirely sure why SVN_ERR_BAD_FILENAME is checked
             ### for here.  That seems to have been introduced in r16840,
             ### and only in r30868 was the APR_EACCES check introduced. */
          if (err->apr_err != SVN_ERR_BAD_FILENAME
              && ! APR_STATUS_IS_EACCES(err->apr_err))
            {
              if (server)
                {
                  /* Called by listening server: Now that we've logged
                   * the error, clear it and return a nice, generic
                   * error to the user
                   * (http://subversion.tigris.org/issues/show_bug.cgi?id=2271). */
                  svn_error_clear(err);
                  return svn_error_create(SVN_ERR_AUTHN_FAILED, NULL, NULL);
                }
              /* Called during startup; return the error, whereupon it
               * will go to standard error for the admin to see. */
              return err;
            }
          else
            /* Ignore SVN_ERR_BAD_FILENAME and APR_EACCES and proceed. */
            svn_error_clear(err);
        }
    }

  /* Read authz configuration. */
  svn_config_get(*cfg, &authzdb_path, SVN_CONFIG_SECTION_GENERAL,
                 SVN_CONFIG_OPTION_AUTHZ_DB, NULL);
  if (authzdb_path)
    {
      authzdb_path = svn_path_join(base, authzdb_path, pool);
      err = svn_repos_authz_read(authzdb, authzdb_path, TRUE, pool);
      if (err)
        {
          if (server)
            {
              /* Called by listening server: Log the error, clear it,
               * and return a nice, generic error to the user
               * (http://subversion.tigris.org/issues/show_bug.cgi?id=2271). */
              log_server_error(err, server, conn, pool);
              svn_error_clear(err);
              return svn_error_create(SVN_ERR_AUTHZ_INVALID_CONFIG, NULL, NULL);
            }
          else
            /* Called during startup; return the error, whereupon it
             * will go to standard error for the admin to see. */
            return err;
        }
    }
  else
    {
      *authzdb = NULL;
    }

  return SVN_NO_ERROR;
}

/* Set *FS_PATH to the portion of URL that is the path within the
   repository, if URL is inside REPOS_URL (if URL is not inside
   REPOS_URL, then error, with the effect on *FS_PATH undefined).

   If the resultant fs path would be the empty string (i.e., URL and
   REPOS_URL are the same), then set *FS_PATH to "/".

   Assume that REPOS_URL and URL are already URI-decoded. */
static svn_error_t *get_fs_path(const char *repos_url, const char *url,
                                const char **fs_path)
{
  apr_size_t len;

  len = strlen(repos_url);
  if (strncmp(url, repos_url, len) != 0)
    return svn_error_createf(SVN_ERR_RA_ILLEGAL_URL, NULL,
                             "'%s' is not the same repository as '%s'",
                             url, repos_url);
  *fs_path = url + len;
  if (! **fs_path)
    *fs_path = "/";

  return SVN_NO_ERROR;
}

/* --- AUTHENTICATION AND AUTHORIZATION FUNCTIONS --- */

/* Set *ALLOWED to TRUE if PATH is accessible in the REQUIRED mode to
   the user described in BATON according to the authz rules in BATON.
   Use POOL for temporary allocations only.  If no authz rules are
   present in BATON, grant access by default. */
static svn_error_t *authz_check_access(svn_boolean_t *allowed,
                                       const char *path,
                                       svn_repos_authz_access_t required,
                                       server_baton_t *b,
                                       apr_pool_t *pool)
{
  /* If authz cannot be performed, grant access.  This is NOT the same
     as the default policy when authz is performed on a path with no
     rules.  In the latter case, the default is to deny access, and is
     set by svn_repos_authz_check_access. */
  if (!b->authzdb)
    {
      *allowed = TRUE;
      return SVN_NO_ERROR;
    }

  /* If the authz request is for the empty path (ie. ""), replace it
     with the root path.  This happens because of stripping done at
     various levels in svnserve that remove the leading / on an
     absolute path. Passing such a malformed path to the authz
     routines throws them into an infinite loop and makes them miss
     ACLs. */
  if (path && *path != '/')
    path = svn_path_join("/", path, pool);

  return svn_repos_authz_check_access(b->authzdb, b->authz_repos_name,
                                      path, b->user, required,
                                      allowed, pool);
}

/* Set *ALLOWED to TRUE if PATH is readable by the user described in
 * BATON.  Use POOL for temporary allocations only.  ROOT is not used.
 * Implements the svn_repos_authz_func_t interface.
 */
static svn_error_t *authz_check_access_cb(svn_boolean_t *allowed,
                                          svn_fs_root_t *root,
                                          const char *path,
                                          void *baton,
                                          apr_pool_t *pool)
{
  server_baton_t *sb = baton;

  return authz_check_access(allowed, path, svn_authz_read, sb, pool);
}

/* If authz is enabled in the specified BATON, return a read authorization
   function. Otherwise, return NULL. */
static svn_repos_authz_func_t authz_check_access_cb_func(server_baton_t *baton)
{
  if (baton->authzdb)
     return authz_check_access_cb;
  return NULL;
}

/* Set *ALLOWED to TRUE if the REQUIRED access to PATH is granted,
 * according to the state in BATON.  Use POOL for temporary
 * allocations only.  ROOT is not used.  Implements the
 * svn_repos_authz_callback_t interface.
 */
static svn_error_t *authz_commit_cb(svn_repos_authz_access_t required,
                                    svn_boolean_t *allowed,
                                    svn_fs_root_t *root,
                                    const char *path,
                                    void *baton,
                                    apr_pool_t *pool)
{
  server_baton_t *sb = baton;

  return authz_check_access(allowed, path, required, sb, pool);
}


enum access_type get_access(server_baton_t *b, enum authn_type auth)
{
  const char *var = (auth == AUTHENTICATED) ? SVN_CONFIG_OPTION_AUTH_ACCESS :
    SVN_CONFIG_OPTION_ANON_ACCESS;
  const char *val, *def = (auth == AUTHENTICATED) ? "write" : "read";
  enum access_type result;

  svn_config_get(b->cfg, &val, SVN_CONFIG_SECTION_GENERAL, var, def);
  result = (strcmp(val, "write") == 0 ? WRITE_ACCESS :
            strcmp(val, "read") == 0 ? READ_ACCESS : NO_ACCESS);
  return (result == WRITE_ACCESS && b->read_only) ? READ_ACCESS : result;
}

static enum access_type current_access(server_baton_t *b)
{
  return get_access(b, (b->user) ? AUTHENTICATED : UNAUTHENTICATED);
}

/* Send authentication mechs for ACCESS_TYPE to the client.  If NEEDS_USERNAME
   is true, don't send anonymous mech even if that would give the desired
   access. */
static svn_error_t *send_mechs(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                               server_baton_t *b, enum access_type required,
                               svn_boolean_t needs_username)
{
  if (!needs_username && get_access(b, UNAUTHENTICATED) >= required)
    SVN_ERR(svn_ra_svn_write_word(conn, pool, "ANONYMOUS"));
  if (b->tunnel_user && get_access(b, AUTHENTICATED) >= required)
    SVN_ERR(svn_ra_svn_write_word(conn, pool, "EXTERNAL"));
  if (b->pwdb && get_access(b, AUTHENTICATED) >= required)
    SVN_ERR(svn_ra_svn_write_word(conn, pool, "CRAM-MD5"));
  return SVN_NO_ERROR;
}

/* Context for cleanup handler. */
struct cleanup_fs_access_baton
{
  svn_fs_t *fs;
  apr_pool_t *pool;
};

/* Pool cleanup handler.  Make sure fs's access_t points to NULL when
   the command pool is destroyed. */
static apr_status_t cleanup_fs_access(void *data)
{
  svn_error_t *serr;
  struct cleanup_fs_access_baton *baton = data;

  serr = svn_fs_set_access(baton->fs, NULL);
  if (serr)
    {
      apr_status_t apr_err = serr->apr_err;
      svn_error_clear(serr);
      return apr_err;
    }

  return APR_SUCCESS;
}


/* Create an svn_fs_access_t in POOL for USER and associate it with
   B's filesystem.  Also, register a cleanup handler with POOL which
   de-associates the svn_fs_access_t from B's filesystem. */
static svn_error_t *
create_fs_access(server_baton_t *b, apr_pool_t *pool)
{
  svn_fs_access_t *fs_access;
  struct cleanup_fs_access_baton *cleanup_baton;

  if (!b->user)
    return SVN_NO_ERROR;

  SVN_ERR(svn_fs_create_access(&fs_access, b->user, pool));
  SVN_ERR(svn_fs_set_access(b->fs, fs_access));

  cleanup_baton = apr_pcalloc(pool, sizeof(*cleanup_baton));
  cleanup_baton->pool = pool;
  cleanup_baton->fs = b->fs;
  apr_pool_cleanup_register(pool, cleanup_baton, cleanup_fs_access,
                            apr_pool_cleanup_null);

  return SVN_NO_ERROR;
}

/* Authenticate, once the client has chosen a mechanism and possibly
 * sent an initial mechanism token.  On success, set *success to true
 * and b->user to the authenticated username (or NULL for anonymous).
 * On authentication failure, report failure to the client and set
 * *success to FALSE.  On communications failure, return an error.
 * If NEEDS_USERNAME is TRUE, don't allow anonymous authentication. */
static svn_error_t *auth(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                         const char *mech, const char *mecharg,
                         server_baton_t *b, enum access_type required,
                         svn_boolean_t needs_username,
                         svn_boolean_t *success)
{
  const char *user;
  *success = FALSE;

  if (get_access(b, AUTHENTICATED) >= required
      && b->tunnel_user && strcmp(mech, "EXTERNAL") == 0)
    {
      if (*mecharg && strcmp(mecharg, b->tunnel_user) != 0)
        return svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure",
                                      "Requested username does not match");
      b->user = b->tunnel_user;
      SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success"));
      *success = TRUE;
      return SVN_NO_ERROR;
    }

  if (get_access(b, UNAUTHENTICATED) >= required
      && strcmp(mech, "ANONYMOUS") == 0 && ! needs_username)
    {
      SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w()", "success"));
      *success = TRUE;
      return SVN_NO_ERROR;
    }

  if (get_access(b, AUTHENTICATED) >= required
      && b->pwdb && strcmp(mech, "CRAM-MD5") == 0)
    {
      SVN_ERR(svn_ra_svn_cram_server(conn, pool, b->pwdb, &user, success));
      b->user = apr_pstrdup(b->pool, user);
      return SVN_NO_ERROR;
    }

  return svn_ra_svn_write_tuple(conn, pool, "w(c)", "failure",
                                "Must authenticate with listed mechanism");
}

/* Perform an authentication request using the built-in SASL implementation. */
static svn_error_t *
internal_auth_request(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                      server_baton_t *b, enum access_type required,
                      svn_boolean_t needs_username)
{
  svn_boolean_t success;
  const char *mech, *mecharg;

  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success"));
  SVN_ERR(send_mechs(conn, pool, b, required, needs_username));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)c)", b->realm));
  do
    {
      SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "w(?c)", &mech, &mecharg));
      if (!*mech)
        break;
      SVN_ERR(auth(conn, pool, mech, mecharg, b, required, needs_username,
                   &success));
    }
  while (!success);
  return SVN_NO_ERROR;
}

/* Perform an authentication request in order to get an access level of
 * REQUIRED or higher.  Since the client may escape the authentication
 * exchange, the caller should check current_access(b) to see if
 * authentication succeeded. */
static svn_error_t *auth_request(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                 server_baton_t *b, enum access_type required,
                                 svn_boolean_t needs_username)
{
#ifdef SVN_HAVE_SASL
  if (b->use_sasl)
    return cyrus_auth_request(conn, pool, b, required, needs_username);
#endif

  return internal_auth_request(conn, pool, b, required, needs_username);
}

/* Send a trivial auth notification on CONN which lists no mechanisms,
 * indicating that authentication is unnecessary.  Usually called in
 * response to invocation of a svnserve command.
 */
static svn_error_t *trivial_auth_request(svn_ra_svn_conn_t *conn,
                                         apr_pool_t *pool, server_baton_t *b)
{
  return svn_ra_svn_write_cmd_response(conn, pool, "()c", "");
}

/* Ensure that the client has the REQUIRED access by checking the
 * access directives (both blanket and per-directory) in BATON.  If
 * PATH is NULL, then only the blanket access configuration will
 * impact the result.
 *
 * If NEEDS_USERNAME is TRUE, then a lookup is only successful if the
 * user described in BATON is authenticated and, well, has a username
 * assigned to him.
 *
 * Use POOL for temporary allocations only.
 */
static svn_boolean_t lookup_access(apr_pool_t *pool,
                                   server_baton_t *baton,
                                   svn_ra_svn_conn_t *conn,
                                   svn_repos_authz_access_t required,
                                   const char *path,
                                   svn_boolean_t needs_username)
{
  enum access_type req = (required & svn_authz_write) ?
    WRITE_ACCESS : READ_ACCESS;
  svn_boolean_t authorized;
  svn_error_t *err;

  /* Get authz's opinion on the access. */
  err = authz_check_access(&authorized, path, required, baton, pool);

  /* If an error made lookup fail, deny access. */
  if (err)
    {
      log_server_error(err, baton, conn, pool);
      svn_error_clear(err);
      return FALSE;
    }

  /* If the required access is blanket-granted AND granted by authz
     AND we already have a username if one is required, then the
     lookup has succeeded. */
  if (current_access(baton) >= req
      && authorized
      && (! needs_username || baton->user))
    return TRUE;

  return FALSE;
}

/* Check that the client has the REQUIRED access by consulting the
 * authentication and authorization states stored in BATON.  If the
 * client does not have the required access credentials, attempt to
 * authenticate the client to get that access, using CONN for
 * communication.
 *
 * This function is supposed to be called to handle the authentication
 * half of a standard svn protocol reply.  If an error is returned, it
 * probably means that the server can terminate the client connection
 * with an apologetic error, as it implies an authentication failure.
 *
 * PATH and NEEDS_USERNAME are passed along to lookup_access, their
 * behaviour is documented there.
 */
static svn_error_t *must_have_access(svn_ra_svn_conn_t *conn,
                                     apr_pool_t *pool,
                                     server_baton_t *b,
                                     svn_repos_authz_access_t required,
                                     const char *path,
                                     svn_boolean_t needs_username)
{
  enum access_type req = (required & svn_authz_write) ?
    WRITE_ACCESS : READ_ACCESS;

  /* See whether the user already has the required access.  If so,
     nothing needs to be done.  Create the FS access and send a
     trivial auth request. */
  if (lookup_access(pool, b, conn, required, path, needs_username))
    {
      SVN_ERR(create_fs_access(b, pool));
      return trivial_auth_request(conn, pool, b);
    }

  /* If the required blanket access can be obtained by authenticating,
     try that.  Unfortunately, we can't tell until after
     authentication whether authz will work or not.  We force
     requiring a username because we need one to be able to check
     authz configuration again with a different user credentials than
     the first time round. */
  if (b->user == NULL
      && get_access(b, AUTHENTICATED) >= req
      && (b->tunnel_user || b->pwdb || b->use_sasl))
    SVN_ERR(auth_request(conn, pool, b, req, TRUE));

  /* Now that an authentication has been done get the new take of
     authz on the request. */
  if (! lookup_access(pool, b, conn, required, path, needs_username))
    return svn_error_create(SVN_ERR_RA_SVN_CMD_ERR,
                            error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED,
                                                 NULL, NULL, b, conn, pool),
                            NULL);

  /* Else, access is granted, and there is much rejoicing. */
  SVN_ERR(create_fs_access(b, pool));

  return SVN_NO_ERROR;
}

/* --- REPORTER COMMAND SET --- */

/* To allow for pipelining, reporter commands have no reponses.  If we
 * get an error, we ignore all subsequent reporter commands and return
 * the error finish_report, to be handled by the calling command.
 */

static svn_error_t *set_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  report_driver_baton_t *b = baton;
  const char *path, *lock_token, *depth_word;
  svn_revnum_t rev;
  /* Default to infinity, for old clients that don't send depth. */
  svn_depth_t depth = svn_depth_infinity;
  svn_boolean_t start_empty;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crb?(?c)?w",
                                 &path, &rev, &start_empty, &lock_token,
                                 &depth_word));
  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  path = svn_path_canonicalize(path, pool);
  if (b->from_rev && strcmp(path, "") == 0)
    *b->from_rev = rev;
  if (!b->err)
    b->err = svn_repos_set_path3(b->report_baton, path, rev, depth,
                                 start_empty, lock_token, pool);
  b->entry_counter++;
  if (!start_empty)
    b->only_empty_entries = FALSE;
  return SVN_NO_ERROR;
}

static svn_error_t *delete_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                apr_array_header_t *params, void *baton)
{
  report_driver_baton_t *b = baton;
  const char *path;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &path));
  path = svn_path_canonicalize(path, pool);
  if (!b->err)
    b->err = svn_repos_delete_path(b->report_baton, path, pool);
  return SVN_NO_ERROR;
}

static svn_error_t *link_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                              apr_array_header_t *params, void *baton)
{
  report_driver_baton_t *b = baton;
  const char *path, *url, *lock_token, *fs_path, *depth_word;
  svn_revnum_t rev;
  svn_boolean_t start_empty;
  /* Default to infinity, for old clients that don't send depth. */
  svn_depth_t depth = svn_depth_infinity;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "ccrb?(?c)?w",
                                 &path, &url, &rev, &start_empty,
                                 &lock_token, &depth_word));
  path = svn_path_canonicalize(path, pool);
  url = svn_path_uri_decode(svn_path_canonicalize(url, pool), pool);
  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  if (!b->err)
    b->err = get_fs_path(svn_path_uri_decode(b->repos_url, pool),
                         url, &fs_path);
  if (!b->err)
    b->err = svn_repos_link_path3(b->report_baton, path, fs_path, rev,
                                  depth, start_empty, lock_token, pool);
  b->entry_counter++;
  return SVN_NO_ERROR;
}

static svn_error_t *finish_report(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  apr_array_header_t *params, void *baton)
{
  report_driver_baton_t *b = baton;

  /* No arguments to parse. */
  SVN_ERR(trivial_auth_request(conn, pool, b->sb));
  if (!b->err)
    b->err = svn_repos_finish_report(b->report_baton, pool);
  return SVN_NO_ERROR;
}

static svn_error_t *abort_report(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                   apr_array_header_t *params, void *baton)
{
  report_driver_baton_t *b = baton;

  /* No arguments to parse. */
  svn_error_clear(svn_repos_abort_report(b->report_baton, pool));
  return SVN_NO_ERROR;
}

static const svn_ra_svn_cmd_entry_t report_commands[] = {
  { "set-path",      set_path },
  { "delete-path",   delete_path },
  { "link-path",     link_path },
  { "finish-report", finish_report, TRUE },
  { "abort-report",  abort_report,  TRUE },
  { NULL }
};

/* Accept a report from the client, drive the network editor with the
 * result, and then write an empty command response.  If there is a
 * non-protocol failure, accept_report will abort the edit and return
 * a command error to be reported by handle_commands().
 *
 * If only_empty_entry is not NULL and the report contains only one
 * item, and that item is empty, set *only_empty_entry to TRUE, else
 * set it to FALSE.
 *
 * If from_rev is not NULL, set *from_rev to the revision number from
 * the set-path on ""; if somehow set-path "" never happens, set
 * *from_rev to SVN_INVALID_REVNUM.
 */
static svn_error_t *accept_report(svn_boolean_t *only_empty_entry,
                                  svn_revnum_t *from_rev,
                                  svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  server_baton_t *b, svn_revnum_t rev,
                                  const char *target, const char *tgt_path,
                                  svn_boolean_t text_deltas,
                                  svn_depth_t depth,
                                  svn_boolean_t send_copyfrom_args,
                                  svn_boolean_t ignore_ancestry)
{
  const svn_delta_editor_t *editor;
  void *edit_baton, *report_baton;
  report_driver_baton_t rb;
  svn_error_t *err;

  /* Make an svn_repos report baton.  Tell it to drive the network editor
   * when the report is complete. */
  svn_ra_svn_get_editor(&editor, &edit_baton, conn, pool, NULL, NULL);
  SVN_CMD_ERR(svn_repos_begin_report2(&report_baton, rev, b->repos,
                                      b->fs_path->data, target, tgt_path,
                                      text_deltas, depth, ignore_ancestry,
                                      send_copyfrom_args,
                                      editor, edit_baton,
                                      authz_check_access_cb_func(b),
                                      b, pool));

  rb.sb = b;
  rb.repos_url = svn_path_uri_decode(b->repos_url, pool);
  rb.report_baton = report_baton;
  rb.err = NULL;
  rb.entry_counter = 0;
  rb.only_empty_entries = TRUE;
  rb.from_rev = from_rev;
  if (from_rev)
    *from_rev = SVN_INVALID_REVNUM;
  err = svn_ra_svn_handle_commands2(conn, pool, report_commands, &rb, TRUE);
  if (err)
    {
      /* Network or protocol error while handling commands. */
      svn_error_clear(rb.err);
      return err;
    }
  else if (rb.err)
    {
      /* Some failure during the reporting or editing operations. */
      SVN_CMD_ERR(rb.err);
    }
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  if (only_empty_entry)
    *only_empty_entry = rb.entry_counter == 1 && rb.only_empty_entries;

  return SVN_NO_ERROR;
}

/* --- MAIN COMMAND SET --- */

/* Write out a list of property diffs.  PROPDIFFS is an array of svn_prop_t
 * values. */
static svn_error_t *write_prop_diffs(svn_ra_svn_conn_t *conn,
                                     apr_pool_t *pool,
                                     apr_array_header_t *propdiffs)
{
  int i;

  for (i = 0; i < propdiffs->nelts; ++i)
    {
      const svn_prop_t *prop = &APR_ARRAY_IDX(propdiffs, i, svn_prop_t);

      SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "c(?s)",
                                     prop->name, prop->value));
    }

  return SVN_NO_ERROR;
}

/* Write out a lock to the client. */
static svn_error_t *write_lock(svn_ra_svn_conn_t *conn,
                               apr_pool_t *pool,
                               svn_lock_t *lock)
{
  const char *cdate, *edate;

  cdate = svn_time_to_cstring(lock->creation_date, pool);
  edate = lock->expiration_date
    ? svn_time_to_cstring(lock->expiration_date, pool) : NULL;
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "ccc(?c)c(?c)", lock->path,
                                 lock->token, lock->owner, lock->comment,
                                 cdate, edate));

  return SVN_NO_ERROR;
}

/* ### This really belongs in libsvn_repos. */
/* Get the properties for a path, with hardcoded committed-info values. */
static svn_error_t *get_props(apr_hash_t **props, svn_fs_root_t *root,
                              const char *path, apr_pool_t *pool)
{
  svn_string_t *str;
  svn_revnum_t crev;
  const char *cdate, *cauthor, *uuid;

  /* Get the properties. */
  SVN_ERR(svn_fs_node_proplist(props, root, path, pool));

  /* Hardcode the values for the committed revision, date, and author. */
  SVN_ERR(svn_repos_get_committed_info(&crev, &cdate, &cauthor, root,
                                       path, pool));
  str = svn_string_create(apr_psprintf(pool, "%ld", crev),
                          pool);
  apr_hash_set(*props, SVN_PROP_ENTRY_COMMITTED_REV, APR_HASH_KEY_STRING, str);
  str = (cdate) ? svn_string_create(cdate, pool) : NULL;
  apr_hash_set(*props, SVN_PROP_ENTRY_COMMITTED_DATE, APR_HASH_KEY_STRING,
               str);
  str = (cauthor) ? svn_string_create(cauthor, pool) : NULL;
  apr_hash_set(*props, SVN_PROP_ENTRY_LAST_AUTHOR, APR_HASH_KEY_STRING, str);

  /* Hardcode the values for the UUID. */
  SVN_ERR(svn_fs_get_uuid(svn_fs_root_fs(root), &uuid, pool));
  str = (uuid) ? svn_string_create(uuid, pool) : NULL;
  apr_hash_set(*props, SVN_PROP_ENTRY_UUID, APR_HASH_KEY_STRING, str);

  return SVN_NO_ERROR;
}

/* Set BATON->FS_PATH for the repository URL found in PARAMS. */
static svn_error_t *reparent(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *url;
  const char *fs_path;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &url));
  url = svn_path_uri_decode(svn_path_canonicalize(url, pool), pool);
  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool),
                          url, &fs_path));
  SVN_ERR(log_command(b, conn, pool, "%s", svn_log__reparent(fs_path, pool)));
  svn_stringbuf_set(b->fs_path, fs_path);
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));
  return SVN_NO_ERROR;
}

static svn_error_t *get_latest_rev(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                   apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;

  SVN_ERR(log_command(b, conn, pool, "get-latest-rev"));

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", rev));
  return SVN_NO_ERROR;
}

static svn_error_t *get_dated_rev(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  apr_time_t tm;
  const char *timestr;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &timestr));
  SVN_ERR(log_command(b, conn, pool, "get-dated-rev %s", timestr));

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(svn_time_from_cstring(&tm, timestr, pool));
  SVN_CMD_ERR(svn_repos_dated_revision(&rev, b->repos, tm, pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", rev));
  return SVN_NO_ERROR;
}

static svn_error_t *change_rev_prop(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                    apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *name;
  svn_string_t *value;

  /* Because the revprop value was at one time mandatory, the usual
     optional element pattern "(?s)" isn't used. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rc?s", &rev, &name, &value));

  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, FALSE));
  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__change_rev_prop(rev, name, pool)));
  SVN_CMD_ERR(svn_repos_fs_change_rev_prop3(b->repos, rev, b->user,
                                            name, value, TRUE, TRUE,
                                            authz_check_access_cb_func(b), b,
                                            pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));
  return SVN_NO_ERROR;
}

static svn_error_t *rev_proplist(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                 apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  apr_hash_t *props;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "r", &rev));
  SVN_ERR(log_command(b, conn, pool, "%s", svn_log__rev_proplist(rev, pool)));

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(svn_repos_fs_revision_proplist(&props, b->repos, rev,
                                             authz_check_access_cb_func(b), b,
                                             pool));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success"));
  SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));
  return SVN_NO_ERROR;
}

static svn_error_t *rev_prop(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *name;
  svn_string_t *value;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rc", &rev, &name));
  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__rev_prop(rev, name, pool)));

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(svn_repos_fs_revision_prop(&value, b->repos, rev, name,
                                         authz_check_access_cb_func(b), b,
                                         pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "(?s)", value));
  return SVN_NO_ERROR;
}

static svn_error_t *commit_done(const svn_commit_info_t *commit_info,
                                void *baton, apr_pool_t *pool)
{
  commit_callback_baton_t *ccb = baton;

  *ccb->new_rev = commit_info->revision;
  *ccb->date = commit_info->date
    ? apr_pstrdup(ccb->pool, commit_info->date): NULL;
  *ccb->author = commit_info->author
    ? apr_pstrdup(ccb->pool, commit_info->author) : NULL;
  *ccb->post_commit_err = commit_info->post_commit_err
    ? apr_pstrdup(ccb->pool, commit_info->post_commit_err) : NULL;
  return SVN_NO_ERROR;
}

/* Add the LOCK_TOKENS (if any) to the filesystem access context,
 * checking path authorizations using the state in SB as we go.
 * LOCK_TOKENS is an array of svn_ra_svn_item_t structs.  Return a
 * client error if LOCK_TOKENS is not a list of lists.  If a lock
 * violates the authz configuration, return SVN_ERR_RA_NOT_AUTHORIZED
 * to the client.  Use POOL for temporary allocations only.
 */
static svn_error_t *add_lock_tokens(svn_ra_svn_conn_t *conn,
                                    apr_array_header_t *lock_tokens,
                                    server_baton_t *sb,
                                    apr_pool_t *pool)
{
  int i;
  svn_fs_access_t *fs_access;

  SVN_ERR(svn_fs_get_access(&fs_access, sb->fs));

  /* If there is no access context, nowhere to add the tokens. */
  if (! fs_access)
    return SVN_NO_ERROR;

  for (i = 0; i < lock_tokens->nelts; ++i)
    {
      const char *path, *token, *full_path;
      svn_ra_svn_item_t *path_item, *token_item;
      svn_ra_svn_item_t *item = &APR_ARRAY_IDX(lock_tokens, i,
                                               svn_ra_svn_item_t);
      if (item->kind != SVN_RA_SVN_LIST)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Lock tokens aren't a list of lists");

      path_item = &APR_ARRAY_IDX(item->u.list, 0, svn_ra_svn_item_t);
      if (path_item->kind != SVN_RA_SVN_STRING)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Lock path isn't a string");

      token_item = &APR_ARRAY_IDX(item->u.list, 1, svn_ra_svn_item_t);
      if (token_item->kind != SVN_RA_SVN_STRING)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Lock token isn't a string");

      path = path_item->u.string->data;
      full_path = svn_path_join(sb->fs_path->data,
                                svn_path_canonicalize(path, pool),
                                pool);

      if (! lookup_access(pool, sb, conn, svn_authz_write,
                          full_path, TRUE))
        return error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, NULL,
                                    sb, conn, pool);

      token = token_item->u.string->data;
      SVN_ERR(svn_fs_access_add_lock_token2(fs_access, path, token));
    }

  return SVN_NO_ERROR;
}

/* Unlock the paths with lock tokens in LOCK_TOKENS, ignoring any errors.
   LOCK_TOKENS contains svn_ra_svn_item_t elements, assumed to be lists. */
static svn_error_t *unlock_paths(apr_array_header_t *lock_tokens,
                                 server_baton_t *sb,
                                 svn_ra_svn_conn_t *conn,
                                 apr_pool_t *pool)
{
  int i;
  apr_pool_t *iterpool;

  iterpool = svn_pool_create(pool);

  for (i = 0; i < lock_tokens->nelts; ++i)
    {
      svn_ra_svn_item_t *item, *path_item, *token_item;
      const char *path, *token, *full_path;
      svn_error_t *err;
      svn_pool_clear(iterpool);

      item = &APR_ARRAY_IDX(lock_tokens, i, svn_ra_svn_item_t);
      path_item = &APR_ARRAY_IDX(item->u.list, 0, svn_ra_svn_item_t);
      token_item = &APR_ARRAY_IDX(item->u.list, 1, svn_ra_svn_item_t);

      path = path_item->u.string->data;
      token = token_item->u.string->data;

      full_path = svn_path_join(sb->fs_path->data,
                                svn_path_canonicalize(path, iterpool),
                                iterpool);

      /* The lock may have become defunct after the commit, so ignore such
         errors. */
      err = svn_repos_fs_unlock(sb->repos, full_path, token,
                                FALSE, iterpool);
      log_server_error(err, sb, conn, iterpool);
      svn_error_clear(err);
    }

  svn_pool_destroy(iterpool);

  return SVN_NO_ERROR;
}

static svn_error_t *commit(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                           apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *log_msg = NULL,
             *date = NULL,
             *author = NULL,
             *post_commit_err = NULL;
  apr_array_header_t *lock_tokens;
  svn_boolean_t keep_locks;
  apr_array_header_t *revprop_list = NULL;
  apr_hash_t *revprop_table;
  const svn_delta_editor_t *editor;
  void *edit_baton;
  svn_boolean_t aborted;
  commit_callback_baton_t ccb;
  svn_revnum_t new_rev;

  if (params->nelts == 1)
    {
      /* Clients before 1.2 don't send lock-tokens, keep-locks,
         and rev-props fields. */
      SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &log_msg));
      lock_tokens = NULL;
      keep_locks = TRUE;
      revprop_list = NULL;
    }
  else
    {
      /* Clients before 1.5 don't send the rev-props field. */
      SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "clb?l", &log_msg,
                                     &lock_tokens, &keep_locks,
                                     &revprop_list));
    }

  /* The handling for locks is a little problematic, because the
     protocol won't let us send several auth requests once one has
     succeeded.  So we request write access and a username before
     adding tokens (if we have any), and subsequently fail if a lock
     violates authz. */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write,
                           NULL,
                           (lock_tokens && lock_tokens->nelts)));

  /* Authorize the lock tokens and give them to the FS if we got
     any. */
  if (lock_tokens && lock_tokens->nelts)
    SVN_CMD_ERR(add_lock_tokens(conn, lock_tokens, b, pool));

  if (revprop_list)
    SVN_ERR(svn_ra_svn_parse_proplist(revprop_list, pool, &revprop_table));
  else
    {
      revprop_table = apr_hash_make(pool);
      apr_hash_set(revprop_table, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
                   svn_string_create(log_msg, pool));
    }

  /* Get author from the baton, making sure clients can't circumvent
     the authentication via the revision props. */
  apr_hash_set(revprop_table, SVN_PROP_REVISION_AUTHOR, APR_HASH_KEY_STRING,
               b->user ? svn_string_create(b->user, pool) : NULL);

  ccb.pool = pool;
  ccb.new_rev = &new_rev;
  ccb.date = &date;
  ccb.author = &author;
  ccb.post_commit_err = &post_commit_err;
  /* ### Note that svn_repos_get_commit_editor5 actually wants a decoded URL. */
  SVN_CMD_ERR(svn_repos_get_commit_editor5
              (&editor, &edit_baton, b->repos, NULL,
               svn_path_uri_decode(b->repos_url, pool),
               b->fs_path->data, revprop_table,
               commit_done, &ccb,
               authz_commit_cb, baton, pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));
  SVN_ERR(svn_ra_svn_drive_editor(conn, pool, editor, edit_baton, &aborted));
  if (!aborted)
    {
      SVN_ERR(log_command(b, conn, pool, "%s",
                          svn_log__commit(new_rev, pool)));
      SVN_ERR(trivial_auth_request(conn, pool, b));

      /* In tunnel mode, deltify before answering the client, because
         answering may cause the client to terminate the connection
         and thus kill the server.  But otherwise, deltify after
         answering the client, to avoid user-visible delay. */

      if (b->tunnel)
        SVN_ERR(svn_fs_deltify_revision(b->fs, new_rev, pool));

      /* Unlock the paths. */
      if (! keep_locks && lock_tokens && lock_tokens->nelts)
        SVN_ERR(unlock_paths(lock_tokens, b, conn, pool));

      SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "r(?c)(?c)(?c)",
                                     new_rev, date, author, post_commit_err));

      if (! b->tunnel)
        SVN_ERR(svn_fs_deltify_revision(b->fs, new_rev, pool));
    }
  return SVN_NO_ERROR;
}

static svn_error_t *get_file(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path, *full_path, *hex_digest;
  svn_revnum_t rev;
  svn_fs_root_t *root;
  svn_stream_t *contents;
  apr_hash_t *props = NULL;
  svn_string_t write_str;
  char buf[4096];
  apr_size_t len;
  svn_boolean_t want_props, want_contents;
  svn_checksum_t *checksum;
  svn_error_t *err, *write_err;

  /* Parse arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)bb", &path, &rev,
                                 &want_props, &want_contents));

  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);

  /* Check authorizations */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read,
                           full_path, FALSE));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__get_file(full_path, rev,
                                        want_contents, want_props, pool)));

  /* Fetch the properties and a stream for the contents. */
  SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool));
  SVN_CMD_ERR(svn_fs_file_checksum(&checksum, svn_checksum_md5, root,
                                   full_path, TRUE, pool));
  hex_digest = svn_checksum_to_cstring_display(checksum, pool);
  if (want_props)
    SVN_CMD_ERR(get_props(&props, root, full_path, pool));
  if (want_contents)
    SVN_CMD_ERR(svn_fs_file_contents(&contents, root, full_path, pool));

  /* Send successful command response with revision and props. */
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((?c)r(!", "success",
                                 hex_digest, rev));
  SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));

  /* Now send the file's contents. */
  if (want_contents)
    {
      err = SVN_NO_ERROR;
      while (1)
        {
          len = sizeof(buf);
          err = svn_stream_read(contents, buf, &len);
          if (err)
            break;
          if (len > 0)
            {
              write_str.data = buf;
              write_str.len = len;
              SVN_ERR(svn_ra_svn_write_string(conn, pool, &write_str));
            }
          if (len < sizeof(buf))
            {
              err = svn_stream_close(contents);
              break;
            }
        }
      write_err = svn_ra_svn_write_cstring(conn, pool, "");
      if (write_err)
        {
          svn_error_clear(err);
          return write_err;
        }
      SVN_CMD_ERR(err);
      SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *get_dir(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                            apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path, *full_path, *file_path, *name, *cauthor, *cdate;
  svn_revnum_t rev;
  apr_hash_t *entries, *props = NULL, *file_props;
  apr_hash_index_t *hi;
  svn_fs_dirent_t *fsent;
  svn_dirent_t *entry;
  const void *key;
  void *val;
  svn_fs_root_t *root;
  apr_pool_t *subpool;
  svn_boolean_t want_props, want_contents;
  apr_uint64_t dirent_fields;
  apr_array_header_t *dirent_fields_list = NULL;
  svn_ra_svn_item_t *elt;
  int i;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)bb?l", &path, &rev,
                                 &want_props, &want_contents,
                                 &dirent_fields_list));

  if (! dirent_fields_list)
    {
      dirent_fields = SVN_DIRENT_ALL;
    }
  else
    {
      dirent_fields = 0;

      for (i = 0; i < dirent_fields_list->nelts; ++i)
        {
          elt = &APR_ARRAY_IDX(dirent_fields_list, i, svn_ra_svn_item_t);

          if (elt->kind != SVN_RA_SVN_WORD)
            return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                    "Dirent field not a string");

          if (strcmp(SVN_RA_SVN_DIRENT_KIND, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_KIND;
          else if (strcmp(SVN_RA_SVN_DIRENT_SIZE, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_SIZE;
          else if (strcmp(SVN_RA_SVN_DIRENT_HAS_PROPS, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_HAS_PROPS;
          else if (strcmp(SVN_RA_SVN_DIRENT_CREATED_REV, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_CREATED_REV;
          else if (strcmp(SVN_RA_SVN_DIRENT_TIME, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_TIME;
          else if (strcmp(SVN_RA_SVN_DIRENT_LAST_AUTHOR, elt->u.word) == 0)
            dirent_fields |= SVN_DIRENT_LAST_AUTHOR;
        }
    }

  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);

  /* Check authorizations */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read,
                           full_path, FALSE));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__get_dir(full_path, rev,
                                       want_contents, want_props,
                                       dirent_fields, pool)));

  /* Fetch the root of the appropriate revision. */
  SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool));

  /* Fetch the directory properties if requested. */
  if (want_props)
    SVN_CMD_ERR(get_props(&props, root, full_path, pool));

  /* Fetch the directory entries if requested. */
  if (want_contents)
    {
      SVN_CMD_ERR(svn_fs_dir_entries(&entries, root, full_path, pool));

      /* Transform the hash table's FS entries into dirents.  This probably
       * belongs in libsvn_repos. */
      subpool = svn_pool_create(pool);
      for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
        {
          apr_hash_this(hi, &key, NULL, &val);
          name = key;
          fsent = val;

          svn_pool_clear(subpool);

          file_path = svn_path_join(full_path, name, subpool);
          entry = apr_pcalloc(pool, sizeof(*entry));

          if (dirent_fields & SVN_DIRENT_KIND)
            {
              /* kind */
              entry->kind = fsent->kind;
            }

          if (dirent_fields & SVN_DIRENT_SIZE)
            {
              /* size */
              if (entry->kind == svn_node_dir)
                entry->size = 0;
              else
                SVN_CMD_ERR(svn_fs_file_length(&entry->size, root, file_path,
                                               subpool));
            }

          if (dirent_fields & SVN_DIRENT_HAS_PROPS)
            {
              /* has_props */
              SVN_CMD_ERR(svn_fs_node_proplist(&file_props, root, file_path,
                                               subpool));
              entry->has_props = (apr_hash_count(file_props) > 0);
            }

          if ((dirent_fields & SVN_DIRENT_LAST_AUTHOR)
              || (dirent_fields & SVN_DIRENT_TIME)
              || (dirent_fields & SVN_DIRENT_CREATED_REV))
            {
              /* created_rev, last_author, time */
              SVN_CMD_ERR(svn_repos_get_committed_info(&entry->created_rev,
                                                       &cdate,
                                                       &cauthor, root,
                                                       file_path,
                                                       subpool));
              entry->last_author = apr_pstrdup(pool, cauthor);
              if (cdate)
                SVN_CMD_ERR(svn_time_from_cstring(&entry->time, cdate,
                                                  subpool));
              else
                entry->time = (time_t) -1;
            }

          /* Store the entry. */
          apr_hash_set(entries, name, APR_HASH_KEY_STRING, entry);
        }
      svn_pool_destroy(subpool);
    }

  /* Write out response. */
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(r(!", "success", rev));
  SVN_ERR(svn_ra_svn_write_proplist(conn, pool, props));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)(!"));
  if (want_contents)
    {
      for (hi = apr_hash_first(pool, entries); hi; hi = apr_hash_next(hi))
        {
          apr_hash_this(hi, &key, NULL, &val);
          name = key;
          entry = val;
          cdate = (entry->time == (time_t) -1) ? NULL
            : svn_time_to_cstring(entry->time, pool);
          SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "cwnbr(?c)(?c)", name,
                                         svn_node_kind_to_word(entry->kind),
                                         (apr_uint64_t) entry->size,
                                         entry->has_props, entry->created_rev,
                                         cdate, entry->last_author));
        }
    }
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));
  return SVN_NO_ERROR;
}

static svn_error_t *update(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                           apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *target, *full_path, *depth_word;
  svn_boolean_t recurse;
  svn_boolean_t send_copyfrom_args;
  apr_uint64_t send_copyfrom_param;
  /* Default to unknown.  Old clients won't send depth, but we'll
     handle that by converting recurse if necessary. */
  svn_depth_t depth = svn_depth_unknown;
  svn_boolean_t is_checkout;

  /* Parse the arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cb?wB", &rev, &target,
                                 &recurse, &depth_word, &send_copyfrom_param));
  target = svn_path_canonicalize(target, pool);

  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  else
    depth = SVN_DEPTH_INFINITY_OR_FILES(recurse);

  send_copyfrom_args = (send_copyfrom_param == SVN_RA_SVN_UNSPECIFIED_NUMBER) ?
      FALSE : (svn_boolean_t) send_copyfrom_param;

  full_path = svn_path_join(b->fs_path->data, target, pool);
  /* Check authorization and authenticate the user if necessary. */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read, full_path, FALSE));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_ERR(accept_report(&is_checkout, NULL,
                        conn, pool, b, rev, target, NULL, TRUE,
                        depth, send_copyfrom_args, FALSE));
  if (is_checkout)
    {
      SVN_ERR(log_command(b, conn, pool, "%s",
                          svn_log__checkout(full_path, rev,
                                            depth, pool)));
    }
  else
    {
      SVN_ERR(log_command(b, conn, pool, "%s",
                          svn_log__update(full_path, rev, depth,
                                          send_copyfrom_args, pool)));
    }

  return SVN_NO_ERROR;
}

static svn_error_t *switch_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                               apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *target, *depth_word;
  const char *switch_url, *switch_path;
  svn_boolean_t recurse;
  /* Default to unknown.  Old clients won't send depth, but we'll
     handle that by converting recurse if necessary. */
  svn_depth_t depth = svn_depth_unknown;

  /* Parse the arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbc?w", &rev, &target,
                                 &recurse, &switch_url, &depth_word));
  target = svn_path_canonicalize(target, pool);
  switch_url = svn_path_canonicalize(switch_url, pool);

  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  else
    depth = SVN_DEPTH_INFINITY_OR_FILES(recurse);

  SVN_ERR(trivial_auth_request(conn, pool, b));
  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool),
                          svn_path_uri_decode(switch_url, pool),
                          &switch_path));

  {
    const char *full_path = svn_path_join(b->fs_path->data, target, pool);
    SVN_ERR(log_command(b, conn, pool, "%s",
                        svn_log__switch(full_path, switch_path, rev,
                                        depth, pool)));
  }

  return accept_report(NULL, NULL,
                       conn, pool, b, rev, target, switch_path, TRUE,
                       depth,
                       FALSE /* TODO(sussman): no copyfrom args for now */,
                       TRUE);
}

static svn_error_t *status(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                           apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *target, *depth_word;
  svn_boolean_t recurse;
  /* Default to unknown.  Old clients won't send depth, but we'll
     handle that by converting recurse if necessary. */
  svn_depth_t depth = svn_depth_unknown;

  /* Parse the arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "cb?(?r)?w",
                                 &target, &recurse, &rev, &depth_word));
  target = svn_path_canonicalize(target, pool);

  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  else
    depth = SVN_DEPTH_INFINITY_OR_EMPTY(recurse);

  SVN_ERR(trivial_auth_request(conn, pool, b));
  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  {
    const char *full_path = svn_path_join(b->fs_path->data, target, pool);
    SVN_ERR(log_command(b, conn, pool, "%s",
                        svn_log__status(full_path, rev, depth, pool)));
  }

  return accept_report(NULL, NULL, conn, pool, b, rev, target, NULL, FALSE,
                       depth, FALSE, FALSE);
}

static svn_error_t *diff(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                         apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *target, *versus_url, *versus_path, *depth_word;
  svn_boolean_t recurse, ignore_ancestry;
  svn_boolean_t text_deltas;
  /* Default to unknown.  Old clients won't send depth, but we'll
     handle that by converting recurse if necessary. */
  svn_depth_t depth = svn_depth_unknown;

  /* Parse the arguments. */
  if (params->nelts == 5)
    {
      /* Clients before 1.4 don't send the text_deltas boolean or depth. */
      SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbbc", &rev, &target,
                                     &recurse, &ignore_ancestry, &versus_url));
      text_deltas = TRUE;
      depth_word = NULL;
    }
  else
    {
      SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?r)cbbcb?w",
                                     &rev, &target, &recurse,
                                     &ignore_ancestry, &versus_url,
                                     &text_deltas, &depth_word));
    }
  target = svn_path_canonicalize(target, pool);
  versus_url = svn_path_canonicalize(versus_url, pool);

  if (depth_word)
    depth = svn_depth_from_word(depth_word);
  else
    depth = SVN_DEPTH_INFINITY_OR_FILES(recurse);

  SVN_ERR(trivial_auth_request(conn, pool, b));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));
  SVN_CMD_ERR(get_fs_path(svn_path_uri_decode(b->repos_url, pool),
                          svn_path_uri_decode(versus_url, pool),
                          &versus_path));

  {
    const char *full_path = svn_path_join(b->fs_path->data, target, pool);
    svn_revnum_t from_rev;
    SVN_ERR(accept_report(NULL, &from_rev,
                          conn, pool, b, rev, target, versus_path,
                          text_deltas, depth, FALSE, ignore_ancestry));
    SVN_ERR(log_command(b, conn, pool, "%s",
                        svn_log__diff(full_path, from_rev, versus_path,
                                      rev, depth, ignore_ancestry,
                                      pool)));
  }
  return SVN_NO_ERROR;
}

/* Regardless of whether a client's capabilities indicate an
   understanding of this command (by way of SVN_RA_SVN_CAP_MERGEINFO),
   we provide a response.

   ASSUMPTION: When performing a 'merge' with two URLs at different
   revisions, the client will call this command more than once. */
static svn_error_t *get_mergeinfo(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  apr_array_header_t *paths, *canonical_paths;
  svn_mergeinfo_catalog_t mergeinfo;
  int i;
  apr_hash_index_t *hi;
  const char *inherit_word;
  svn_mergeinfo_inheritance_t inherit;
  svn_boolean_t include_descendants;
  apr_pool_t *iterpool;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "l(?r)wb", &paths, &rev,
                                 &inherit_word, &include_descendants));
  inherit = svn_inheritance_from_word(inherit_word);

  /* Canonicalize the paths which mergeinfo has been requested for. */
  canonical_paths = apr_array_make(pool, paths->nelts, sizeof(const char *));
  for (i = 0; i < paths->nelts; i++)
     {
        svn_ra_svn_item_t *item = &APR_ARRAY_IDX(paths, i, svn_ra_svn_item_t);
        const char *full_path;

        if (item->kind != SVN_RA_SVN_STRING)
          return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                  _("Path is not a string"));
        full_path = svn_path_join(b->fs_path->data,
                                  svn_path_canonicalize(item->u.string->data,
                                                        pool),
                                  pool);
        APR_ARRAY_PUSH(canonical_paths, const char *) = full_path;
     }

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__get_mergeinfo(canonical_paths, inherit,
                                             include_descendants,
                                             pool)));

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_CMD_ERR(svn_repos_fs_get_mergeinfo(&mergeinfo, b->repos,
                                         canonical_paths, rev,
                                         inherit,
                                         include_descendants,
                                         authz_check_access_cb_func(b), b,
                                         pool));
  SVN_ERR(svn_mergeinfo__remove_prefix_from_catalog(&mergeinfo, mergeinfo,
                                                    b->fs_path->data, pool));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success"));
  iterpool = svn_pool_create(pool);
  for (hi = apr_hash_first(pool, mergeinfo); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *value;
      svn_string_t *mergeinfo_string;

      svn_pool_clear(iterpool);

      apr_hash_this(hi, &key, NULL, &value);
      SVN_ERR(svn_mergeinfo_to_string(&mergeinfo_string,
                                      (svn_mergeinfo_t) value,
                                      iterpool));
      SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "cs", (const char *) key,
                                     mergeinfo_string));
    }
  svn_pool_destroy(iterpool);
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));

  return SVN_NO_ERROR;
}

/* Send a log entry to the client. */
static svn_error_t *log_receiver(void *baton,
                                 svn_log_entry_t *log_entry,
                                 apr_pool_t *pool)
{
  log_baton_t *b = baton;
  svn_ra_svn_conn_t *conn = b->conn;
  apr_hash_index_t *h;
  const void *key;
  void *val;
  const char *path;
  svn_log_changed_path2_t *change;
  svn_boolean_t invalid_revnum = FALSE;
  char action[2];
  const char *author, *date, *message;
  apr_uint64_t revprop_count;

  if (log_entry->revision == SVN_INVALID_REVNUM)
    {
      /* If the stack depth is zero, we've seen the last revision, so don't
         send it, just return. */
      if (b->stack_depth == 0)
        return SVN_NO_ERROR;

      /* Because the svn protocol won't let us send an invalid revnum, we have
         to fudge here and send an additional flag. */
      log_entry->revision = 0;
      invalid_revnum = TRUE;
      b->stack_depth--;
    }

  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "(!"));
  if (log_entry->changed_paths2)
    {
      for (h = apr_hash_first(pool, log_entry->changed_paths2); h;
                                                        h = apr_hash_next(h))
        {
          apr_hash_this(h, &key, NULL, &val);
          path = key;
          change = val;
          action[0] = change->action;
          action[1] = '\0';
          SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "cw(?cr)(?c)", path,
                                         action, change->copyfrom_path,
                                         change->copyfrom_rev,
                                         svn_node_kind_to_word(
                                                          change->node_kind)));
        }
    }
  svn_compat_log_revprops_out(&author, &date, &message, log_entry->revprops);
  svn_compat_log_revprops_clear(log_entry->revprops);
  if (log_entry->revprops)
    revprop_count = apr_hash_count(log_entry->revprops);
  else
    revprop_count = 0;
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)r(?c)(?c)(?c)bbn(!",
                                 log_entry->revision,
                                 author, date, message,
                                 log_entry->has_children,
                                 invalid_revnum, revprop_count));
  SVN_ERR(svn_ra_svn_write_proplist(conn, pool, log_entry->revprops));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)"));

  if (log_entry->has_children)
    b->stack_depth++;

  return SVN_NO_ERROR;
}

static svn_error_t *log_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                            apr_array_header_t *params, void *baton)
{
  svn_error_t *err, *write_err;
  server_baton_t *b = baton;
  svn_revnum_t start_rev, end_rev;
  const char *full_path;
  svn_boolean_t changed_paths, strict_node, include_merged_revisions;
  apr_array_header_t *paths, *full_paths, *revprop_items, *revprops;
  char *revprop_word;
  svn_ra_svn_item_t *elt;
  int i;
  apr_uint64_t limit, include_merged_revs_param;
  log_baton_t lb;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "l(?r)(?r)bb?n?Bwl", &paths,
                                 &start_rev, &end_rev, &changed_paths,
                                 &strict_node, &limit,
                                 &include_merged_revs_param,
                                 &revprop_word, &revprop_items));

  if (include_merged_revs_param == SVN_RA_SVN_UNSPECIFIED_NUMBER)
    include_merged_revisions = FALSE;
  else
    include_merged_revisions = (svn_boolean_t) include_merged_revs_param;

  if (revprop_word == NULL)
    /* pre-1.5 client */
    revprops = svn_compat_log_revprops_in(pool);
  else if (strcmp(revprop_word, "all-revprops") == 0)
    revprops = NULL;
  else if (strcmp(revprop_word, "revprops") == 0)
    {
      revprops = apr_array_make(pool, revprop_items->nelts,
                                sizeof(char *));
      if (revprop_items)
        {
          for (i = 0; i < revprop_items->nelts; i++)
            {
              elt = &APR_ARRAY_IDX(revprop_items, i, svn_ra_svn_item_t);
              if (elt->kind != SVN_RA_SVN_STRING)
                return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                        _("Log revprop entry not a string"));
              APR_ARRAY_PUSH(revprops, const char *) = elt->u.string->data;
            }
        }
    }
  else
    return svn_error_createf(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                             _("Unknown revprop word '%s' in log command"),
                             revprop_word);

  /* If we got an unspecified number then the user didn't send us anything,
     so we assume no limit.  If it's larger than INT_MAX then someone is
     messing with us, since we know the svn client libraries will never send
     us anything that big, so play it safe and default to no limit. */
  if (limit == SVN_RA_SVN_UNSPECIFIED_NUMBER || limit > INT_MAX)
    limit = 0;

  full_paths = apr_array_make(pool, paths->nelts, sizeof(const char *));
  for (i = 0; i < paths->nelts; i++)
    {
      elt = &APR_ARRAY_IDX(paths, i, svn_ra_svn_item_t);
      if (elt->kind != SVN_RA_SVN_STRING)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                _("Log path entry not a string"));
      full_path = svn_path_join(b->fs_path->data,
                                svn_path_canonicalize(elt->u.string->data,
                                                      pool),
                                pool);
      APR_ARRAY_PUSH(full_paths, const char *) = full_path;
    }
  SVN_ERR(trivial_auth_request(conn, pool, b));

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__log(full_paths, start_rev, end_rev,
                                   limit, changed_paths, strict_node,
                                   include_merged_revisions, revprops,
                                   pool)));

  /* Get logs.  (Can't report errors back to the client at this point.) */
  lb.fs_path = b->fs_path->data;
  lb.conn = conn;
  lb.stack_depth = 0;
  err = svn_repos_get_logs4(b->repos, full_paths, start_rev, end_rev,
                            (int) limit, changed_paths, strict_node,
                            include_merged_revisions, revprops,
                            authz_check_access_cb_func(b), b, log_receiver,
                            &lb, pool);

  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (write_err)
    {
      svn_error_clear(err);
      return write_err;
    }
  SVN_CMD_ERR(err);
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));
  return SVN_NO_ERROR;
}

static svn_error_t *check_path(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                               apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *path, *full_path;
  svn_fs_root_t *root;
  svn_node_kind_t kind;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)", &path, &rev));
  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);

  /* Check authorizations */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read,
                           full_path, FALSE));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_ERR(log_command(b, conn, pool, "check-path %s@%d",
                      svn_path_uri_encode(full_path, pool), rev));

  SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool));
  SVN_CMD_ERR(svn_fs_check_path(&kind, root, full_path, pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "w",
                                        svn_node_kind_to_word(kind)));
  return SVN_NO_ERROR;
}

static svn_error_t *stat_cmd(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_revnum_t rev;
  const char *path, *full_path, *cdate;
  svn_fs_root_t *root;
  svn_dirent_t *dirent;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)", &path, &rev));
  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);

  /* Check authorizations */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read,
                           full_path, FALSE));

  if (!SVN_IS_VALID_REVNUM(rev))
    SVN_CMD_ERR(svn_fs_youngest_rev(&rev, b->fs, pool));

  SVN_ERR(log_command(b, conn, pool, "stat %s@%d",
                      svn_path_uri_encode(full_path, pool), rev));

  SVN_CMD_ERR(svn_fs_revision_root(&root, b->fs, rev, pool));
  SVN_CMD_ERR(svn_repos_stat(&dirent, root, full_path, pool));

  /* Need to return the equivalent of "(?l)", since that's what the
     client is reading.  */

  if (dirent == NULL)
    {
      SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "()"));
      return SVN_NO_ERROR;
    }

  cdate = (dirent->time == (time_t) -1) ? NULL
    : svn_time_to_cstring(dirent->time, pool);

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "((wnbr(?c)(?c)))",
                                        svn_node_kind_to_word(dirent->kind),
                                        (apr_uint64_t) dirent->size,
                                        dirent->has_props, dirent->created_rev,
                                        cdate, dirent->last_author));

  return SVN_NO_ERROR;
}

static svn_error_t *get_locations(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  apr_array_header_t *params, void *baton)
{
  svn_error_t *err, *write_err;
  server_baton_t *b = baton;
  svn_revnum_t revision;
  apr_array_header_t *location_revisions, *loc_revs_proto;
  svn_ra_svn_item_t *elt;
  int i;
  const char *relative_path;
  svn_revnum_t peg_revision;
  apr_hash_t *fs_locations;
  apr_hash_index_t *iter;
  const char *abs_path;
  const void *iter_key;
  void *iter_value;

  /* Parse the arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crl", &relative_path,
                                 &peg_revision,
                                 &loc_revs_proto));
  relative_path = svn_path_canonicalize(relative_path, pool);

  abs_path = svn_path_join(b->fs_path->data, relative_path, pool);

  location_revisions = apr_array_make(pool, loc_revs_proto->nelts,
                                      sizeof(svn_revnum_t));
  for (i = 0; i < loc_revs_proto->nelts; i++)
    {
      elt = &APR_ARRAY_IDX(loc_revs_proto, i, svn_ra_svn_item_t);
      if (elt->kind != SVN_RA_SVN_NUMBER)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Get-locations location revisions entry "
                                "not a revision number");
      revision = (svn_revnum_t)(elt->u.number);
      APR_ARRAY_PUSH(location_revisions, svn_revnum_t) = revision;
    }
  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__get_locations(abs_path, peg_revision,
                                             location_revisions, pool)));

  /* All the parameters are fine - let's perform the query against the
   * repository. */

  /* We store both err and write_err here, so the client will get
   * the "done" even if there was an error in fetching the results. */

  err = svn_repos_trace_node_locations(b->fs, &fs_locations, abs_path,
                                       peg_revision, location_revisions,
                                       authz_check_access_cb_func(b), b, pool);

  /* Now, write the results to the connection. */
  if (!err)
    {
      if (fs_locations)
        {
          for (iter = apr_hash_first(pool, fs_locations); iter;
              iter = apr_hash_next(iter))
            {
              apr_hash_this(iter, &iter_key, NULL, &iter_value);
              SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "rc",
                                             *(const svn_revnum_t *)iter_key,
                                             (const char *)iter_value));
            }
        }
    }

  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (write_err)
    {
      svn_error_clear(err);
      return write_err;
    }
  SVN_CMD_ERR(err);

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *gls_receiver(svn_location_segment_t *segment,
                                 void *baton,
                                 apr_pool_t *pool)
{
  svn_ra_svn_conn_t *conn = baton;
  return svn_ra_svn_write_tuple(conn, pool, "rr(?c)",
                                segment->range_start,
                                segment->range_end,
                                segment->path);
}

static svn_error_t *get_location_segments(svn_ra_svn_conn_t *conn,
                                          apr_pool_t *pool,
                                          apr_array_header_t *params,
                                          void *baton)
{
  svn_error_t *err, *write_err;
  server_baton_t *b = baton;
  svn_revnum_t peg_revision, start_rev, end_rev;
  const char *relative_path;
  const char *abs_path;

  /* Parse the arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)(?r)(?r)",
                                 &relative_path, &peg_revision,
                                 &start_rev, &end_rev));
  relative_path = svn_path_canonicalize(relative_path, pool);

  abs_path = svn_path_join(b->fs_path->data, relative_path, pool);

  if (SVN_IS_VALID_REVNUM(start_rev)
      && SVN_IS_VALID_REVNUM(end_rev)
      && (end_rev > start_rev))
    return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                             "Get-location-segments end revision must not be "
                             "younger than start revision");

  if (SVN_IS_VALID_REVNUM(peg_revision)
      && SVN_IS_VALID_REVNUM(start_rev)
      && (start_rev > peg_revision))
    return svn_error_createf(SVN_ERR_INCORRECT_PARAMS, NULL,
                             "Get-location-segments start revision must not "
                             "be younger than peg revision");

  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_ERR(log_command(baton, conn, pool, "%s",
                      svn_log__get_location_segments(abs_path, peg_revision,
                                                     start_rev, end_rev,
                                                     pool)));

  /* All the parameters are fine - let's perform the query against the
   * repository. */

  /* We store both err and write_err here, so the client will get
   * the "done" even if there was an error in fetching the results. */

  err = svn_repos_node_location_segments(b->repos, abs_path,
                                         peg_revision, start_rev, end_rev,
                                         gls_receiver, (void *)conn,
                                         authz_check_access_cb_func(b), b,
                                         pool);
  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (write_err)
    {
      svn_error_clear(err);
      return write_err;
    }
  SVN_CMD_ERR(err);

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

/* This implements svn_write_fn_t.  Write LEN bytes starting at DATA to the
   client as a string. */
static svn_error_t *svndiff_handler(void *baton, const char *data,
                                    apr_size_t *len)
{
  file_revs_baton_t *b = baton;
  svn_string_t str;

  str.data = data;
  str.len = *len;
  return svn_ra_svn_write_string(b->conn, b->pool, &str);
}

/* This implements svn_close_fn_t.  Mark the end of the data by writing an
   empty string to the client. */
static svn_error_t *svndiff_close_handler(void *baton)
{
  file_revs_baton_t *b = baton;

  SVN_ERR(svn_ra_svn_write_cstring(b->conn, b->pool, ""));
  return SVN_NO_ERROR;
}

/* This implements the svn_repos_file_rev_handler_t interface. */
static svn_error_t *file_rev_handler(void *baton, const char *path,
                                     svn_revnum_t rev, apr_hash_t *rev_props,
                                     svn_boolean_t merged_revision,
                                     svn_txdelta_window_handler_t *d_handler,
                                     void **d_baton,
                                     apr_array_header_t *prop_diffs,
                                     apr_pool_t *pool)
{
  file_revs_baton_t *frb = baton;
  svn_stream_t *stream;

  SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "cr(!",
                                 path, rev));
  SVN_ERR(svn_ra_svn_write_proplist(frb->conn, pool, rev_props));
  SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "!)(!"));
  SVN_ERR(write_prop_diffs(frb->conn, pool, prop_diffs));
  SVN_ERR(svn_ra_svn_write_tuple(frb->conn, pool, "!)b", merged_revision));

  /* Store the pool for the delta stream. */
  frb->pool = pool;

  /* Prepare for the delta or just write an empty string. */
  if (d_handler)
    {
      stream = svn_stream_create(baton, pool);
      svn_stream_set_write(stream, svndiff_handler);
      svn_stream_set_close(stream, svndiff_close_handler);

      if (svn_ra_svn_has_capability(frb->conn, SVN_RA_SVN_CAP_SVNDIFF1))
        svn_txdelta_to_svndiff2(d_handler, d_baton, stream, 1, pool);
      else
        svn_txdelta_to_svndiff2(d_handler, d_baton, stream, 0, pool);
    }
  else
    SVN_ERR(svn_ra_svn_write_cstring(frb->conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *get_file_revs(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                  apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_error_t *err, *write_err;
  file_revs_baton_t frb;
  svn_revnum_t start_rev, end_rev;
  const char *path;
  const char *full_path;
  apr_uint64_t include_merged_revs_param;
  svn_boolean_t include_merged_revisions;

  /* Parse arguments. */
  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?r)(?r)?B",
                                 &path, &start_rev, &end_rev,
                                 &include_merged_revs_param));
  path = svn_path_canonicalize(path, pool);
  SVN_ERR(trivial_auth_request(conn, pool, b));
  full_path = svn_path_join(b->fs_path->data, path, pool);

  if (include_merged_revs_param == SVN_RA_SVN_UNSPECIFIED_NUMBER)
    include_merged_revisions = FALSE;
  else
    include_merged_revisions = (svn_boolean_t) include_merged_revs_param;

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__get_file_revs(full_path, start_rev, end_rev,
                                             include_merged_revisions,
                                             pool)));

  frb.conn = conn;
  frb.pool = NULL;

  err = svn_repos_get_file_revs2(b->repos, full_path, start_rev, end_rev,
                                 include_merged_revisions,
                                 authz_check_access_cb_func(b), b,
                                 file_rev_handler, &frb, pool);
  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (write_err)
    {
      svn_error_clear(err);
      return write_err;
    }
  SVN_CMD_ERR(err);
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *lock(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                         apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path;
  const char *comment;
  const char *full_path;
  svn_boolean_t steal_lock;
  svn_revnum_t current_rev;
  svn_lock_t *l;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?c)b(?r)", &path, &comment,
                                 &steal_lock, &current_rev));
  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);

  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write,
                           full_path, TRUE));
  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__lock_one_path(full_path, steal_lock, pool)));

  SVN_CMD_ERR(svn_repos_fs_lock(&l, b->repos, full_path, NULL, comment, 0,
                                0, /* No expiration time. */
                                current_rev, steal_lock, pool));

  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(!", "success"));
  SVN_ERR(write_lock(conn, pool, l));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!)"));

  return SVN_NO_ERROR;
}

static svn_error_t *lock_many(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                              apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  apr_array_header_t *path_revs;
  const char *comment;
  svn_boolean_t steal_lock;
  int i;
  apr_pool_t *subpool;
  const char *path;
  const char *full_path;
  svn_revnum_t current_rev;
  apr_array_header_t *log_paths;
  svn_lock_t *l;
  svn_error_t *err = SVN_NO_ERROR, *write_err;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "(?c)bl", &comment, &steal_lock,
                                 &path_revs));

  subpool = svn_pool_create(pool);

  /* Because we can only send a single auth reply per request, we send
     a reply before parsing the lock commands.  This means an authz
     access denial will abort the processing of the locks and return
     an error. */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, TRUE));

  /* Loop through the lock requests. */
  log_paths = apr_array_make(pool, path_revs->nelts, sizeof(full_path));
  for (i = 0; i < path_revs->nelts; ++i)
    {
      svn_ra_svn_item_t *item = &APR_ARRAY_IDX(path_revs, i,
                                               svn_ra_svn_item_t);

      svn_pool_clear(subpool);

      if (item->kind != SVN_RA_SVN_LIST)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Lock requests should be list of lists");

      SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, pool, "c(?r)", &path,
                                     &current_rev));

      /* Allocate the full_path out of pool so it will survive for use
       * by operational logging, after this loop. */
      full_path = svn_path_join(b->fs_path->data,
                                svn_path_canonicalize(path, subpool),
                                pool);
      APR_ARRAY_PUSH(log_paths, const char *) = full_path;

      if (! lookup_access(pool, b, conn, svn_authz_write, full_path, TRUE))
        {
          err = error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL, NULL,
                                     b, conn, pool);
          break;
        }

      err = svn_repos_fs_lock(&l, b->repos, full_path,
                              NULL, comment, FALSE,
                              0, /* No expiration time. */
                              current_rev,
                              steal_lock, subpool);

      if (err)
        {
          if (SVN_ERR_IS_LOCK_ERROR(err))
            {
              write_err = svn_ra_svn_write_cmd_failure(conn, pool, err);
              svn_error_clear(err);
              err = NULL;
              SVN_ERR(write_err);
            }
          else
            break;
        }
      else
        {
          SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "w!", "success"));
          SVN_ERR(write_lock(conn, subpool, l));
          SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "!"));
        }
    }

  svn_pool_destroy(subpool);

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__lock(log_paths, steal_lock, pool)));

  /* NOTE: err might contain a fatal locking error from the loop above. */
  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (!write_err)
    SVN_CMD_ERR(err);
  svn_error_clear(err);
  SVN_ERR(write_err);
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *unlock(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                           apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path, *token, *full_path;
  svn_boolean_t break_lock;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c(?c)b", &path, &token,
                                 &break_lock));

  full_path = svn_path_join(b->fs_path->data, svn_path_canonicalize(path, pool),
                            pool);

  /* Username required unless break_lock was specified. */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write,
                           full_path, ! break_lock));
  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__unlock_one_path(full_path, break_lock, pool)));

  SVN_CMD_ERR(svn_repos_fs_unlock(b->repos, full_path, token, break_lock,
                                  pool));

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *unlock_many(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  svn_boolean_t break_lock;
  apr_array_header_t *unlock_tokens;
  int i;
  apr_pool_t *subpool;
  const char *path;
  const char *full_path;
  apr_array_header_t *log_paths;
  const char *token;
  svn_error_t *err = SVN_NO_ERROR, *write_err;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "bl", &break_lock,
                                 &unlock_tokens));

  /* Username required unless break_lock was specified. */
  SVN_ERR(must_have_access(conn, pool, b, svn_authz_write, NULL, ! break_lock));

  subpool = svn_pool_create(pool);

  /* Loop through the unlock requests. */
  log_paths = apr_array_make(pool, unlock_tokens->nelts, sizeof(full_path));
  for (i = 0; i < unlock_tokens->nelts; i++)
    {
      svn_ra_svn_item_t *item = &APR_ARRAY_IDX(unlock_tokens, i,
                                               svn_ra_svn_item_t);

      svn_pool_clear(subpool);

      if (item->kind != SVN_RA_SVN_LIST)
        return svn_error_create(SVN_ERR_RA_SVN_MALFORMED_DATA, NULL,
                                "Unlock request should be a list of lists");

      SVN_ERR(svn_ra_svn_parse_tuple(item->u.list, subpool, "c(?c)", &path,
                                     &token));

      /* Allocate the full_path out of pool so it will survive for use
       * by operational logging, after this loop. */
      full_path = svn_path_join(b->fs_path->data,
                                svn_path_canonicalize(path, subpool),
                                pool);
      APR_ARRAY_PUSH(log_paths, const char *) = full_path;

      if (! lookup_access(subpool, b, conn, svn_authz_write, full_path,
                          ! break_lock))
        return svn_error_create(SVN_ERR_RA_SVN_CMD_ERR,
                                error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED,
                                                     NULL, NULL,
                                                     b, conn, pool),
                                NULL);

      err = svn_repos_fs_unlock(b->repos, full_path, token, break_lock,
                                subpool);
      if (err)
        {
          if (SVN_ERR_IS_UNLOCK_ERROR(err))
            {
              write_err = svn_ra_svn_write_cmd_failure(conn, pool, err);
              svn_error_clear(err);
              err = NULL;
              SVN_ERR(write_err);
            }
          else
            break;
        }
      else
        SVN_ERR(svn_ra_svn_write_tuple(conn, subpool, "w(c)", "success",
                                       path));
    }

  svn_pool_destroy(subpool);

  SVN_ERR(log_command(b, conn, pool, "%s",
                      svn_log__unlock(log_paths, break_lock, pool)));

  /* NOTE: err might contain a fatal unlocking error from the loop above. */
  write_err = svn_ra_svn_write_word(conn, pool, "done");
  if (! write_err)
    SVN_CMD_ERR(err);
  svn_error_clear(err);
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *get_lock(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                             apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path;
  const char *full_path;
  svn_lock_t *l;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &path));

  full_path = svn_path_join(b->fs_path->data, svn_path_canonicalize(path,
                                                                    pool),
                            pool);

  SVN_ERR(must_have_access(conn, pool, b, svn_authz_read,
                           full_path, FALSE));
  SVN_ERR(log_command(b, conn, pool, "get-lock %s",
                      svn_path_uri_encode(full_path, pool)));

  SVN_CMD_ERR(svn_fs_get_lock(&l, b->fs, full_path, pool));

  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success"));
  if (l)
    SVN_ERR(write_lock(conn, pool, l));
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));

  return SVN_NO_ERROR;
}

static svn_error_t *get_locks(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                              apr_array_header_t *params, void *baton)
{
  server_baton_t *b = baton;
  const char *path;
  const char *full_path;
  apr_hash_t *locks;
  apr_hash_index_t *hi;
  void *val;
  svn_lock_t *l;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "c", &path));

  full_path = svn_path_join(b->fs_path->data, svn_path_canonicalize(path,
                                                                    pool),
                            pool);

  SVN_ERR(trivial_auth_request(conn, pool, b));

  SVN_ERR(log_command(b, conn, pool, "get-locks %s",
                      svn_path_uri_encode(full_path, pool)));
  SVN_CMD_ERR(svn_repos_fs_get_locks(&locks, b->repos, full_path,
                                     authz_check_access_cb_func(b), b, pool));

  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w((!", "success"));
  for (hi = apr_hash_first(pool, locks); hi; hi = apr_hash_next(hi))
    {
      apr_hash_this(hi, NULL, NULL, &val);
      l = val;
      SVN_ERR(write_lock(conn, pool, l));
    }
  SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));

  return SVN_NO_ERROR;
}

static svn_error_t *replay_one_revision(svn_ra_svn_conn_t *conn,
                                        server_baton_t *b,
                                        svn_revnum_t rev,
                                        svn_revnum_t low_water_mark,
                                        svn_boolean_t send_deltas,
                                        apr_pool_t *pool)
{
  const svn_delta_editor_t *editor;
  void *edit_baton;
  svn_fs_root_t *root;
  svn_error_t *err;

  SVN_ERR(log_command(b, conn, pool,
                      svn_log__replay(b->fs_path->data, low_water_mark,
                                      pool)));

  svn_ra_svn_get_editor(&editor, &edit_baton, conn, pool, NULL, NULL);

  err = svn_fs_revision_root(&root, b->fs, rev, pool);

  if (! err)
    err = svn_repos_replay2(root, b->fs_path->data, low_water_mark,
                            send_deltas, editor, edit_baton,
                            authz_check_access_cb_func(b), b, pool);

  if (err)
    svn_error_clear(editor->abort_edit(edit_baton, pool));
  SVN_CMD_ERR(err);

  return svn_ra_svn_write_cmd(conn, pool, "finish-replay", "");
}

static svn_error_t *replay(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                           apr_array_header_t *params, void *baton)
{
  svn_revnum_t rev, low_water_mark;
  svn_boolean_t send_deltas;
  server_baton_t *b = baton;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rrb", &rev, &low_water_mark,
                                 &send_deltas));

  SVN_ERR(trivial_auth_request(conn, pool, b));

  SVN_ERR(replay_one_revision(conn, b, rev, low_water_mark,
                              send_deltas, pool));

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *replay_range(svn_ra_svn_conn_t *conn, apr_pool_t *pool,
                                 apr_array_header_t *params, void *baton)
{
  svn_revnum_t start_rev, end_rev, rev, low_water_mark;
  svn_boolean_t send_deltas;
  server_baton_t *b = baton;
  apr_pool_t *iterpool;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "rrrb", &start_rev,
                                 &end_rev, &low_water_mark,
                                 &send_deltas));

  SVN_ERR(trivial_auth_request(conn, pool, b));

  iterpool = svn_pool_create(pool);
  for (rev = start_rev; rev <= end_rev; rev++)
    {
      apr_hash_t *props;

      svn_pool_clear(iterpool);

      SVN_CMD_ERR(svn_repos_fs_revision_proplist(&props, b->repos, rev,
                                                 authz_check_access_cb_func(b),
                                                 b,
                                                 iterpool));
      SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "w(!", "revprops"));
      SVN_ERR(svn_ra_svn_write_proplist(conn, iterpool, props));
      SVN_ERR(svn_ra_svn_write_tuple(conn, iterpool, "!)"));

      SVN_ERR(replay_one_revision(conn, b, rev, low_water_mark,
                                  send_deltas, iterpool));

    }
  svn_pool_destroy(iterpool);

  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, ""));

  return SVN_NO_ERROR;
}

static svn_error_t *
get_deleted_rev(svn_ra_svn_conn_t *conn,
                apr_pool_t *pool,
                apr_array_header_t *params,
                void *baton)
{
  server_baton_t *b = baton;
  const char *path, *full_path;
  svn_revnum_t peg_revision;
  svn_revnum_t end_revision;
  svn_revnum_t revision_deleted;

  SVN_ERR(svn_ra_svn_parse_tuple(params, pool, "crr",
                                 &path, &peg_revision, &end_revision));
  full_path = svn_path_join(b->fs_path->data,
                            svn_path_canonicalize(path, pool), pool);
  SVN_ERR(log_command(b, conn, pool, "get-deleted-rev"));
  SVN_ERR(trivial_auth_request(conn, pool, b));
  SVN_ERR(svn_repos_deleted_rev(b->fs, full_path, peg_revision, end_revision,
                                &revision_deleted, pool));
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "r", revision_deleted));
  return SVN_NO_ERROR;
}

static const svn_ra_svn_cmd_entry_t main_commands[] = {
  { "reparent",        reparent },
  { "get-latest-rev",  get_latest_rev },
  { "get-dated-rev",   get_dated_rev },
  { "change-rev-prop", change_rev_prop },
  { "rev-proplist",    rev_proplist },
  { "rev-prop",        rev_prop },
  { "commit",          commit },
  { "get-file",        get_file },
  { "get-dir",         get_dir },
  { "update",          update },
  { "switch",          switch_cmd },
  { "status",          status },
  { "diff",            diff },
  { "get-mergeinfo",   get_mergeinfo },
  { "log",             log_cmd },
  { "check-path",      check_path },
  { "stat",            stat_cmd },
  { "get-locations",   get_locations },
  { "get-location-segments",   get_location_segments },
  { "get-file-revs",   get_file_revs },
  { "lock",            lock },
  { "lock-many",       lock_many },
  { "unlock",          unlock },
  { "unlock-many",     unlock_many },
  { "get-lock",        get_lock },
  { "get-locks",       get_locks },
  { "replay",          replay },
  { "replay-range",    replay_range },
  { "get-deleted-rev", get_deleted_rev },
  { NULL }
};

/* Skip past the scheme part of a URL, including the tunnel specification
 * if present.  Return NULL if the scheme part is invalid for ra_svn. */
static const char *skip_scheme_part(const char *url)
{
  if (strncmp(url, "svn", 3) != 0)
    return NULL;
  url += 3;
  if (*url == '+')
    url += strcspn(url, ":");
  if (strncmp(url, "://", 3) != 0)
    return NULL;
  return url + 3;
}

/* Check that PATH is a valid repository path, meaning it doesn't contain any
   '..' path segments.
   NOTE: This is similar to svn_path_is_backpath_present, but that function
   assumes the path separator is '/'.  This function also checks for
   segments delimited by the local path separator. */
static svn_boolean_t
repos_path_valid(const char *path)
{
  const char *s = path;

  while (*s)
    {
      /* Scan for the end of the segment. */
      while (*path && *path != '/' && *path != SVN_PATH_LOCAL_SEPARATOR)
        ++path;

      /* Check for '..'. */
#ifdef WIN32
      /* On Windows, don't allow sequences of more than one character
         consisting of just dots and spaces.  Win32 functions treat
         paths such as ".. " and "......." inconsistently.  Make sure
         no one can escape out of the root. */
      if (path - s >= 2 && strspn(s, ". ") == path - s)
        return FALSE;
#else  /* ! WIN32 */
      if (path - s == 2 && s[0] == '.' && s[1] == '.')
        return FALSE;
#endif

      /* Skip all separators. */
      while (*path && (*path == '/' || *path == SVN_PATH_LOCAL_SEPARATOR))
        ++path;
      s = path;
    }

  return TRUE;
}

/* Look for the repository given by URL, using ROOT as the virtual
 * repository root.  If we find one, fill in the repos, fs, cfg,
 * repos_url, and fs_path fields of B.  Set B->repos's client
 * capabilities to CAPABILITIES, which must be at least as long-lived
 * as POOL, and whose elements are SVN_RA_CAPABILITY_*.
 */
static svn_error_t *find_repos(const char *url, const char *root,
                               server_baton_t *b,
                               svn_ra_svn_conn_t *conn,
                               apr_array_header_t *capabilities,
                               apr_pool_t *pool)
{
  const char *path, *full_path, *repos_root, *fs_path;
  svn_stringbuf_t *url_buf;

  /* Skip past the scheme and authority part. */
  path = skip_scheme_part(url);
  if (path == NULL)
    return svn_error_createf(SVN_ERR_BAD_URL, NULL,
                             "Non-svn URL passed to svn server: '%s'", url);
  path = strchr(path, '/');
  path = (path == NULL) ? "" : path + 1;

  /* Decode URI escapes from the path. */
  path = svn_path_uri_decode(path, pool);

  /* Ensure that it isn't possible to escape the root by skipping leading
     slashes and not allowing '..' segments. */
  while (*path == '/')
    ++path;
  if (!repos_path_valid(path))
    return svn_error_create(SVN_ERR_BAD_FILENAME, NULL,
                            "Couldn't determine repository path");

  /* Join the server-configured root with the client path. */
  full_path = svn_path_join(svn_path_canonicalize(root, pool),
                            svn_path_canonicalize(path, pool), pool);

  /* Search for a repository in the full path. */
  repos_root = svn_repos_find_root_path(full_path, pool);
  if (!repos_root)
    return svn_error_createf(SVN_ERR_RA_SVN_REPOS_NOT_FOUND, NULL,
                             "No repository found in '%s'", url);

  /* Open the repository and fill in b with the resulting information. */
  SVN_ERR(svn_repos_open(&b->repos, repos_root, pool));
  SVN_ERR(svn_repos_remember_client_capabilities(b->repos, capabilities));
  b->fs = svn_repos_fs(b->repos);
  fs_path = full_path + strlen(repos_root);
  b->fs_path = svn_stringbuf_create(*fs_path ? fs_path : "/", pool);
  url_buf = svn_stringbuf_create(url, pool);
  svn_path_remove_components(url_buf,
                             svn_path_component_count(b->fs_path->data));
  b->repos_url = url_buf->data;
  b->authz_repos_name = svn_path_is_child(root, repos_root, pool);
  if (b->authz_repos_name == NULL)
    b->repos_name = svn_path_basename(repos_root, pool);
  else
    b->repos_name = b->authz_repos_name;
  b->repos_name = svn_path_uri_encode(b->repos_name, pool);

  /* If the svnserve configuration files have not been loaded then
     load them from the repository. */
  if (NULL == b->cfg)
    SVN_ERR(load_configs(&b->cfg, &b->pwdb, &b->authzdb,
                         svn_repos_svnserve_conf(b->repos, pool), FALSE,
                         svn_repos_conf_dir(b->repos, pool),
                         b, conn,
                         pool));

#ifdef SVN_HAVE_SASL
  /* Should we use Cyrus SASL? */
  svn_config_get_bool(b->cfg, &b->use_sasl, SVN_CONFIG_SECTION_SASL,
                      SVN_CONFIG_OPTION_USE_SASL, FALSE);
#endif

  /* Use the repository UUID as the default realm. */
  SVN_ERR(svn_fs_get_uuid(b->fs, &b->realm, pool));
  svn_config_get(b->cfg, &b->realm, SVN_CONFIG_SECTION_GENERAL,
                 SVN_CONFIG_OPTION_REALM, b->realm);

  /* Make sure it's possible for the client to authenticate.  Note
     that this doesn't take into account any authz configuration read
     above, because we can't know about access it grants until paths
     are given by the client. */
  if (get_access(b, UNAUTHENTICATED) == NO_ACCESS
      && (get_access(b, AUTHENTICATED) == NO_ACCESS
          || (!b->tunnel_user && !b->pwdb && !b->use_sasl)))
    return error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                                 "No access allowed to this repository",
                                 b, conn, pool);

  return SVN_NO_ERROR;
}

/* Compute the authentication name EXTERNAL should be able to get, if any. */
static const char *get_tunnel_user(serve_params_t *params, apr_pool_t *pool)
{
  /* Only offer EXTERNAL for connections tunneled over a login agent. */
  if (!params->tunnel)
    return NULL;

  /* If a tunnel user was provided on the command line, use that. */
  if (params->tunnel_user)
    return params->tunnel_user;

  return svn_user_get_name(pool);
}

static void
fs_warning_func(void *baton, svn_error_t *err)
{
  fs_warning_baton_t *b = baton;
  log_server_error(err, b->server, b->conn, b->pool);
  /* TODO: Keep log_pool in the server baton, cleared after every log? */
  svn_pool_clear(b->pool);
}

svn_error_t *serve(svn_ra_svn_conn_t *conn, serve_params_t *params,
                   apr_pool_t *pool)
{
  svn_error_t *err, *io_err;
  apr_uint64_t ver;
  const char *uuid, *client_url, *ra_client_string, *client_string;
  apr_array_header_t *caplist, *cap_words;
  server_baton_t b;
  fs_warning_baton_t warn_baton;
  svn_stringbuf_t *cap_log = svn_stringbuf_create("", pool);

  b.tunnel = params->tunnel;
  b.tunnel_user = get_tunnel_user(params, pool);
  b.read_only = params->read_only;
  b.user = NULL;
  b.cfg = params->cfg;
  b.pwdb = params->pwdb;
  b.authzdb = params->authzdb;
  b.realm = NULL;
  b.log_file = params->log_file;
  b.pool = pool;
  b.use_sasl = FALSE;

  /* Send greeting.  We don't support version 1 any more, so we can
   * send an empty mechlist. */
  SVN_ERR(svn_ra_svn_write_cmd_response(conn, pool, "nn()(wwwwwww)",
                                        (apr_uint64_t) 2, (apr_uint64_t) 2,
                                        SVN_RA_SVN_CAP_EDIT_PIPELINE,
                                        SVN_RA_SVN_CAP_SVNDIFF1,
                                        SVN_RA_SVN_CAP_ABSENT_ENTRIES,
                                        SVN_RA_SVN_CAP_COMMIT_REVPROPS,
                                        SVN_RA_SVN_CAP_DEPTH,
                                        SVN_RA_SVN_CAP_LOG_REVPROPS,
                                        SVN_RA_SVN_CAP_PARTIAL_REPLAY));

  /* Read client response, which we assume to be in version 2 format:
   * version, capability list, and client URL; then we do an auth
   * request. */
  SVN_ERR(svn_ra_svn_read_tuple(conn, pool, "nlc?c(?c)",
                                &ver, &caplist, &client_url,
                                &ra_client_string,
                                &client_string));
  if (ver != 2)
    return SVN_NO_ERROR;

  client_url = svn_path_canonicalize(client_url, pool);
  SVN_ERR(svn_ra_svn_set_capabilities(conn, caplist));

  /* All released versions of Subversion support edit-pipeline,
   * so we do not accept connections from clients that do not. */
  if (! svn_ra_svn_has_capability(conn, SVN_RA_SVN_CAP_EDIT_PIPELINE))
    return SVN_NO_ERROR;

  /* find_repos needs the capabilities as a list of words (eventually
     they get handed to the start-commit hook).  While we could add a
     new interface to re-retrieve them from conn and convert the
     result to a list, it's simpler to just convert caplist by hand
     here, since we already have it and turning 'svn_ra_svn_item_t's
     into 'const char *'s is pretty easy.

     We only record capabilities we care about.  The client may report
     more (because it doesn't know what the server cares about). */
  {
    int i;
    svn_ra_svn_item_t *item;

    cap_words = apr_array_make(pool, 1, sizeof(const char *));
    for (i = 0; i < caplist->nelts; i++)
      {
        item = &APR_ARRAY_IDX(caplist, i, svn_ra_svn_item_t);
        /* ra_svn_set_capabilities() already type-checked for us */
        if (strcmp(item->u.word, SVN_RA_SVN_CAP_MERGEINFO) == 0)
          {
            APR_ARRAY_PUSH(cap_words, const char *)
              = SVN_RA_CAPABILITY_MERGEINFO;
          }
        /* Save for operational log. */
        if (cap_log->len > 0)
          svn_stringbuf_appendcstr(cap_log, " ");
        svn_stringbuf_appendcstr(cap_log, item->u.word);
      }
  }

  err = find_repos(client_url, params->root, &b, conn, cap_words, pool);
  if (!err)
    {
      SVN_ERR(auth_request(conn, pool, &b, READ_ACCESS, FALSE));
      if (current_access(&b) == NO_ACCESS)
        err = error_create_and_log(SVN_ERR_RA_NOT_AUTHORIZED, NULL,
                                   "Not authorized for access",
                                   &b, conn, pool);
    }
  if (err)
    {
      log_error(err, b.log_file, svn_ra_svn_conn_remote_host(conn),
                b.user, NULL, pool);
      io_err = svn_ra_svn_write_cmd_failure(conn, pool, err);
      svn_error_clear(err);
      SVN_ERR(io_err);
      return svn_ra_svn_flush(conn, pool);
    }

  /* Log the open. */
  if (ra_client_string == NULL || ra_client_string[0] == '\0')
    ra_client_string = "-";
  else
    ra_client_string = svn_path_uri_encode(ra_client_string, pool);
  if (client_string == NULL || client_string[0] == '\0')
    client_string = "-";
  else
    client_string = svn_path_uri_encode(client_string, pool);
  SVN_ERR(log_command(&b, conn, pool,
                      "open %" APR_UINT64_T_FMT " cap=(%s) %s %s %s",
                      ver, cap_log->data,
                      svn_path_uri_encode(b.fs_path->data, pool),
                      ra_client_string, client_string));

  warn_baton.server = &b;
  warn_baton.conn = conn;
  warn_baton.pool = svn_pool_create(pool);
  svn_fs_set_warning_func(b.fs, fs_warning_func, &warn_baton);

  SVN_ERR(svn_fs_get_uuid(b.fs, &uuid, pool));

  /* We can't claim mergeinfo capability until we know whether the
     repository supports mergeinfo (i.e., is not a 1.4 repository),
     but we don't get the repository url from the client until after
     we've already sent the initial list of server capabilities.  So
     we list repository capabilities here, in our first response after
     the client has sent the url. */
  {
    svn_boolean_t supports_mergeinfo;
    SVN_ERR(svn_repos_has_capability(b.repos, &supports_mergeinfo,
                                     SVN_REPOS_CAPABILITY_MERGEINFO, pool));

    SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "w(cc(!",
                                   "success", uuid, b.repos_url));
    if (supports_mergeinfo)
      SVN_ERR(svn_ra_svn_write_word(conn, pool, SVN_RA_SVN_CAP_MERGEINFO));
    SVN_ERR(svn_ra_svn_write_tuple(conn, pool, "!))"));
  }

  return svn_ra_svn_handle_commands2(conn, pool, main_commands, &b, FALSE);
}