/*
 * io.c:   shared file reading, writing, and probing code.
 *
 * ====================================================================
 *    Licensed to the Apache Software Foundation (ASF) under one
 *    or more contributor license agreements.  See the NOTICE file
 *    distributed with this work for additional information
 *    regarding copyright ownership.  The ASF licenses this file
 *    to you under the Apache License, Version 2.0 (the
 *    "License"); you may not use this file except in compliance
 *    with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing,
 *    software distributed under the License is distributed on an
 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *    KIND, either express or implied.  See the License for the
 *    specific language governing permissions and limitations
 *    under the License.
 * ====================================================================
 */



#include <stdio.h>

#ifndef WIN32
#include <unistd.h>
#endif

#ifdef __CYGWIN__
#include <errno.h>
#endif

#ifndef APR_STATUS_IS_EPERM
#include <errno.h>
#ifdef EPERM
#define APR_STATUS_IS_EPERM(s)   ((s) == EPERM)
#else
#define APR_STATUS_IS_EPERM(s)   (0)
#endif
#endif

#include <apr_lib.h>
#include <apr_pools.h>
#include <apr_file_io.h>
#include <apr_file_info.h>
#include <apr_general.h>
#include <apr_strings.h>
#include <apr_portable.h>
#include <apr_md5.h>

#if APR_HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include "svn_hash.h"
#include "svn_types.h"
#include "svn_dirent_uri.h"
#include "svn_path.h"
#include "svn_string.h"
#include "svn_error.h"
#include "svn_io.h"
#include "svn_pools.h"
#include "svn_utf.h"
#include "svn_config.h"
#include "svn_private_config.h"
#include "svn_ctype.h"

#include "private/svn_atomic.h"
#include "private/svn_io_private.h"
#include "private/svn_utf_private.h"
#include "private/svn_dep_compat.h"

#define SVN_SLEEP_ENV_VAR "SVN_I_LOVE_CORRUPTED_WORKING_COPIES_SO_DISABLE_SLEEP_FOR_TIMESTAMPS"

/*
  Windows is 'aided' by a number of types of applications that
  follow other applications around and open up files they have
  changed for various reasons (the most intrusive are virus
  scanners).  So, if one of these other apps has glommed onto
  our file we may get an 'access denied' error.

  This retry loop does not completely solve the problem (who
  knows how long the other app is going to hold onto it for), but
  goes a long way towards minimizing it.  It is not an infinite
  loop because there might really be an error.

  Another reason for retrying delete operations on Windows
  is that they are asynchronous -- the file or directory is not
  actually deleted until the last handle to it is closed.  The
  retry loop cannot completely solve this problem either, but can
  help mitigate it.
*/
#define RETRY_MAX_ATTEMPTS 100
#define RETRY_INITIAL_SLEEP 1000
#define RETRY_MAX_SLEEP 128000

#define RETRY_LOOP(err, expr, retry_test, sleep_test)                      \
  do                                                                       \
    {                                                                      \
      apr_status_t os_err = APR_TO_OS_ERROR(err);                          \
      int sleep_count = RETRY_INITIAL_SLEEP;                               \
      int retries;                                                         \
      for (retries = 0;                                                    \
           retries < RETRY_MAX_ATTEMPTS && (retry_test);                   \
           os_err = APR_TO_OS_ERROR(err))                                  \
        {                                                                  \
          if (sleep_test)                                                  \
            {                                                              \
              ++retries;                                                   \
              apr_sleep(sleep_count);                                      \
              if (sleep_count < RETRY_MAX_SLEEP)                           \
                sleep_count *= 2;                                          \
            }                                                              \
          (err) = (expr);                                                  \
        }                                                                  \
    }                                                                      \
  while (0)

#if defined(EDEADLK) && APR_HAS_THREADS
#define FILE_LOCK_RETRY_LOOP(err, expr)                                    \
  RETRY_LOOP(err,                                                          \
             expr,                                                         \
             (APR_STATUS_IS_EINTR(err) || os_err == EDEADLK),              \
             (!APR_STATUS_IS_EINTR(err)))
#else
#define FILE_LOCK_RETRY_LOOP(err, expr)                                    \
  RETRY_LOOP(err,                                                          \
             expr,                                                         \
             (APR_STATUS_IS_EINTR(err)),                                   \
             0)
#endif

#ifndef WIN32_RETRY_LOOP
#if defined(WIN32) && !defined(SVN_NO_WIN32_RETRY_LOOP)
#define WIN32_RETRY_LOOP(err, expr)                                        \
  RETRY_LOOP(err, expr, (os_err == ERROR_ACCESS_DENIED                     \
                         || os_err == ERROR_SHARING_VIOLATION              \
                         || os_err == ERROR_DIR_NOT_EMPTY),                \
             1)
#elif defined(__CYGWIN__)
#define WIN32_RETRY_LOOP(err, expr)                                        \
  RETRY_LOOP(err, expr, (os_err == EBUSY                                   \
                         || os_err == EACCES                               \
                         || os_err == ENOTEMPTY),                          \
             1)
#else
#define WIN32_RETRY_LOOP(err, expr) ((void)0)
#endif
#endif

#ifdef WIN32

#if _WIN32_WINNT < 0x600 /* Does the SDK assume Windows Vista+? */
typedef struct _FILE_RENAME_INFO {
  BOOL   ReplaceIfExists;
  HANDLE RootDirectory;
  DWORD  FileNameLength;
  WCHAR  FileName[1];
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

typedef struct _FILE_DISPOSITION_INFO {
  BOOL DeleteFile;
} FILE_DISPOSITION_INFO, *PFILE_DISPOSITION_INFO;

#define FileRenameInfo 3
#define FileDispositionInfo 4
#endif /* WIN32 < Vista */

/* One-time initialization of the late bound Windows API functions. */
static volatile svn_atomic_t win_dynamic_imports_state = 0;

/* Pointer to GetFinalPathNameByHandleW function from kernel32.dll. */
typedef DWORD (WINAPI *GETFINALPATHNAMEBYHANDLE)(
               HANDLE hFile,
               WCHAR *lpszFilePath,
               DWORD cchFilePath,
               DWORD dwFlags);

typedef BOOL (WINAPI *SetFileInformationByHandle_t)(HANDLE hFile,
                                                    int FileInformationClass,
                                                    LPVOID lpFileInformation,
                                                    DWORD dwBufferSize);

static GETFINALPATHNAMEBYHANDLE get_final_path_name_by_handle_proc = NULL;
static SetFileInformationByHandle_t set_file_information_by_handle_proc = NULL;

/* Forward declaration. */
static svn_error_t * io_win_read_link(svn_string_t **dest,
                                      const char *path,
                                      apr_pool_t *pool);

#endif

/* Forward declaration */
static apr_status_t
dir_is_empty(const char *dir, apr_pool_t *pool);
static APR_INLINE svn_error_t *
do_io_file_wrapper_cleanup(apr_file_t *file, apr_status_t status,
                           const char *msg, const char *msg_no_name,
                           apr_pool_t *pool);

/* Local wrapper of svn_path_cstring_to_utf8() that does no copying on
 * operating systems where APR always uses utf-8 as native path format */
static svn_error_t *
cstring_to_utf8(const char **path_utf8,
                const char *path_apr,
                apr_pool_t *pool)
{
#if defined(WIN32) || defined(DARWIN)
  *path_utf8 = path_apr;
  return SVN_NO_ERROR;
#else
  return svn_path_cstring_to_utf8(path_utf8, path_apr, pool);
#endif
}

/* Local wrapper of svn_path_cstring_from_utf8() that does no copying on
 * operating systems where APR always uses utf-8 as native path format */
static svn_error_t *
cstring_from_utf8(const char **path_apr,
                  const char *path_utf8,
                  apr_pool_t *pool)
{
#if defined(WIN32) || defined(DARWIN)
  *path_apr = path_utf8;
  return SVN_NO_ERROR;
#else
  return svn_path_cstring_from_utf8(path_apr, path_utf8, pool);
#endif
}

/* Helper function that allows to convert an APR-level PATH to something
 * that we can pass the svn_error_wrap_apr. Since we use it in context
 * of error reporting, having *some* path info may be more useful than
 * having none.  Therefore, we use a best effort approach here.
 *
 * This is different from svn_io_file_name_get in that it uses a different
 * signature style and will never fail.
 */
static const char *
try_utf8_from_internal_style(const char *path, apr_pool_t *pool)
{
  svn_error_t *error;
  const char *path_utf8;

  /* Special case. */
  if (path == NULL)
    return "(NULL)";

  /* (try to) convert PATH to UTF-8. If that fails, continue with the plain
   * PATH because it is the best we have. It may actually be UTF-8 already.
   */
  error = cstring_to_utf8(&path_utf8, path, pool);
  if (error)
    {
      /* fallback to best representation we have */

      svn_error_clear(error);
      path_utf8 = path;
    }

  /* Toggle (back-)slashes etc. as necessary.
   */
  return svn_dirent_local_style(path_utf8, pool);
}


/* Set *NAME_P to the UTF-8 representation of directory entry NAME.
 * NAME is in the internal encoding used by APR; PARENT is in
 * UTF-8 and in internal (not local) style.
 *
 * Use PARENT only for generating an error string if the conversion
 * fails because NAME could not be represented in UTF-8.  In that
 * case, return a two-level error in which the outer error's message
 * mentions PARENT, but the inner error's message does not mention
 * NAME (except possibly in hex) since NAME may not be printable.
 * Such a compound error at least allows the user to go looking in the
 * right directory for the problem.
 *
 * If there is any other error, just return that error directly.
 *
 * If there is any error, the effect on *NAME_P is undefined.
 *
 * *NAME_P and NAME may refer to the same storage.
 */
static svn_error_t *
entry_name_to_utf8(const char **name_p,
                   const char *name,
                   const char *parent,
                   apr_pool_t *pool)
{
#if defined(WIN32) || defined(DARWIN)
  *name_p = apr_pstrdup(pool, name);
  return SVN_NO_ERROR;
#else
  svn_error_t *err = svn_path_cstring_to_utf8(name_p, name, pool);
  if (err && err->apr_err == APR_EINVAL)
    {
      return svn_error_createf(err->apr_err, err,
                               _("Error converting entry "
                                 "in directory '%s' to UTF-8"),
                               svn_dirent_local_style(parent, pool));
    }
  return err;
#endif
}



static void
map_apr_finfo_to_node_kind(svn_node_kind_t *kind,
                           svn_boolean_t *is_special,
                           apr_finfo_t *finfo)
{
  *is_special = FALSE;

  if (finfo->filetype == APR_REG)
    *kind = svn_node_file;
  else if (finfo->filetype == APR_DIR)
    *kind = svn_node_dir;
  else if (finfo->filetype == APR_LNK)
    {
      *is_special = TRUE;
      *kind = svn_node_file;
    }
  else
    *kind = svn_node_unknown;
}

/* Helper for svn_io_check_path() and svn_io_check_resolved_path();
   essentially the same semantics as those two, with the obvious
   interpretation for RESOLVE_SYMLINKS. */
static svn_error_t *
io_check_path(const char *path,
              svn_boolean_t resolve_symlinks,
              svn_boolean_t *is_special_p,
              svn_node_kind_t *kind,
              apr_pool_t *pool)
{
  apr_int32_t flags;
  apr_finfo_t finfo;
  apr_status_t apr_err;
  const char *path_apr;
  svn_boolean_t is_special = FALSE;

  if (path[0] == '\0')
    path = ".";

  /* Not using svn_io_stat() here because we want to check the
     apr_err return explicitly. */
  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  flags = resolve_symlinks ? APR_FINFO_MIN : (APR_FINFO_MIN | APR_FINFO_LINK);
  apr_err = apr_stat(&finfo, path_apr, flags, pool);

  if (APR_STATUS_IS_ENOENT(apr_err))
    *kind = svn_node_none;
  else if (SVN__APR_STATUS_IS_ENOTDIR(apr_err))
    *kind = svn_node_none;
  else if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't check path '%s'"),
                              svn_dirent_local_style(path, pool));
  else
    map_apr_finfo_to_node_kind(kind, &is_special, &finfo);

  *is_special_p = is_special;

  return SVN_NO_ERROR;
}


/* Wrapper for apr_file_open(), taking an APR-encoded filename. */
static apr_status_t
file_open(apr_file_t **f,
          const char *fname_apr,
          apr_int32_t flag,
          apr_fileperms_t perm,
          svn_boolean_t retry_on_failure,
          apr_pool_t *pool)
{
  apr_status_t status = apr_file_open(f, fname_apr, flag, perm, pool);

  if (retry_on_failure)
    {
#ifdef WIN32
      if (status == APR_FROM_OS_ERROR(ERROR_ACCESS_DENIED))
        {
          if ((flag & (APR_CREATE | APR_EXCL)) == (APR_CREATE | APR_EXCL))
            return status; /* Can't create if there is something */

          if (flag & (APR_WRITE | APR_CREATE))
            {
              apr_finfo_t finfo;

              if (!apr_stat(&finfo, fname_apr, SVN__APR_FINFO_READONLY, pool))
                {
                  if (finfo.protection & APR_FREADONLY)
                    return status; /* Retrying won't fix this */
                }
            }
        }
#endif

      WIN32_RETRY_LOOP(status, apr_file_open(f, fname_apr, flag, perm, pool));
    }
  return status;
}


svn_error_t *
svn_io_check_resolved_path(const char *path,
                           svn_node_kind_t *kind,
                           apr_pool_t *pool)
{
  svn_boolean_t ignored;
  return io_check_path(path, TRUE, &ignored, kind, pool);
}

svn_error_t *
svn_io_check_path(const char *path,
                  svn_node_kind_t *kind,
                  apr_pool_t *pool)
{
  svn_boolean_t ignored;
  return io_check_path(path, FALSE, &ignored, kind, pool);
}

svn_error_t *
svn_io_check_special_path(const char *path,
                          svn_node_kind_t *kind,
                          svn_boolean_t *is_special,
                          apr_pool_t *pool)
{
  return io_check_path(path, FALSE, is_special, kind, pool);
}

struct temp_file_cleanup_s
{
  apr_pool_t *pool;
  /* The (APR-encoded) full path of the file to be removed, or NULL if
   * nothing to do. */
  const char *fname_apr;
};


static apr_status_t
temp_file_plain_cleanup_handler(void *baton)
{
  struct  temp_file_cleanup_s *b = baton;
  apr_status_t apr_err = APR_SUCCESS;

  if (b->fname_apr)
    {
      apr_err = apr_file_remove(b->fname_apr, b->pool);
      WIN32_RETRY_LOOP(apr_err, apr_file_remove(b->fname_apr, b->pool));
    }

  return apr_err;
}


static apr_status_t
temp_file_child_cleanup_handler(void *baton)
{
  struct  temp_file_cleanup_s *b = baton;

  apr_pool_cleanup_kill(b->pool, b,
                        temp_file_plain_cleanup_handler);

  return APR_SUCCESS;
}


svn_error_t *
svn_io_open_uniquely_named(apr_file_t **file,
                           const char **unique_path,
                           const char *dirpath,
                           const char *filename,
                           const char *suffix,
                           svn_io_file_del_t delete_when,
                           apr_pool_t *result_pool,
                           apr_pool_t *scratch_pool)
{
  const char *path;
  unsigned int i;
  struct temp_file_cleanup_s *baton = NULL;

  /* At the beginning, we don't know whether unique_path will need
     UTF8 conversion */
  svn_boolean_t needs_utf8_conversion = TRUE;

  SVN_ERR_ASSERT(file || unique_path);

  if (dirpath == NULL)
    SVN_ERR(svn_io_temp_dir(&dirpath, scratch_pool));
  if (filename == NULL)
    filename = "tempfile";
  if (suffix == NULL)
    suffix = ".tmp";

  path = svn_dirent_join(dirpath, filename, scratch_pool);

  if (delete_when == svn_io_file_del_on_pool_cleanup)
    {
      baton = apr_palloc(result_pool, sizeof(*baton));

      baton->pool = result_pool;
      baton->fname_apr = NULL;

      /* Because cleanups are run LIFO, we need to make sure to register
         our cleanup before the apr_file_close cleanup:

         On Windows, you can't remove an open file.
      */
      apr_pool_cleanup_register(result_pool, baton,
                                temp_file_plain_cleanup_handler,
                                temp_file_child_cleanup_handler);
    }

  for (i = 1; i <= 99999; i++)
    {
      const char *unique_name;
      const char *unique_name_apr;
      apr_file_t *try_file;
      apr_status_t apr_err;
      apr_int32_t flag = (APR_READ | APR_WRITE | APR_CREATE | APR_EXCL
                          | APR_BUFFERED | APR_BINARY);

      if (delete_when == svn_io_file_del_on_close)
        flag |= APR_DELONCLOSE;

      /* Special case the first attempt -- if we can avoid having a
         generated numeric portion at all, that's best.  So first we
         try with just the suffix; then future tries add a number
         before the suffix.  (A do-while loop could avoid the repeated
         conditional, but it's not worth the clarity loss.)

         If the first attempt fails, the first number will be "2".
         This is good, since "1" would misleadingly imply that
         the second attempt was actually the first... and if someone's
         got conflicts on their conflicts, we probably don't want to
         add to their confusion :-). */
      if (i == 1)
        unique_name = apr_psprintf(scratch_pool, "%s%s", path, suffix);
      else
        unique_name = apr_psprintf(scratch_pool, "%s.%u%s", path, i, suffix);

      /* Hmmm.  Ideally, we would append to a native-encoding buf
         before starting iteration, then convert back to UTF-8 for
         return. But I suppose that would make the appending code
         sensitive to i18n in a way it shouldn't be... Oh well. */
      if (needs_utf8_conversion)
        {
          SVN_ERR(cstring_from_utf8(&unique_name_apr, unique_name,
                                    scratch_pool));
          if (i == 1)
            {
              /* The variable parts of unique_name will not require UTF8
                 conversion. Therefore, if UTF8 conversion had no effect
                 on it in the first iteration, it won't require conversion
                 in any future iteration. */
              needs_utf8_conversion = strcmp(unique_name_apr, unique_name);
            }
        }
      else
        unique_name_apr = unique_name;

      apr_err = file_open(&try_file, unique_name_apr, flag,
                          APR_OS_DEFAULT, FALSE, result_pool);

      if (APR_STATUS_IS_EEXIST(apr_err))
        continue;
      else if (apr_err)
        {
          /* On Win32, CreateFile fails with an "Access Denied" error
             code, rather than "File Already Exists", if the colliding
             name belongs to a directory. */
          if (APR_STATUS_IS_EACCES(apr_err))
            {
              apr_finfo_t finfo;
              apr_status_t apr_err_2 = apr_stat(&finfo, unique_name_apr,
                                                APR_FINFO_TYPE, scratch_pool);

              if (!apr_err_2 && finfo.filetype == APR_DIR)
                continue;

#ifdef WIN32
              apr_err_2 = APR_TO_OS_ERROR(apr_err);

              if (apr_err_2 == ERROR_ACCESS_DENIED ||
                  apr_err_2 == ERROR_SHARING_VIOLATION)
                {
                  /* The file is in use by another process or is hidden;
                     create a new name, but don't do this 99999 times in
                     case the folder is not writable */
                  i += 797;
                  continue;
                }
#endif

              /* Else fall through and return the original error. */
            }

          if (file)
            *file = NULL;
          if (unique_path)
            *unique_path = NULL;
          return svn_error_wrap_apr(apr_err, _("Can't open '%s'"),
                                    svn_dirent_local_style(unique_name,
                                                         scratch_pool));
        }
      else
        {
          if (delete_when == svn_io_file_del_on_pool_cleanup)
            baton->fname_apr = apr_pstrdup(result_pool, unique_name_apr);

          if (file)
            *file = try_file;
          else
            apr_file_close(try_file);
          if (unique_path)
            *unique_path = apr_pstrdup(result_pool, unique_name);

          return SVN_NO_ERROR;
        }
    }

  if (file)
    *file = NULL;
  if (unique_path)
    *unique_path = NULL;
  return svn_error_createf(SVN_ERR_IO_UNIQUE_NAMES_EXHAUSTED,
                           NULL,
                           _("Unable to make name for '%s'"),
                           svn_dirent_local_style(path, scratch_pool));
}

svn_error_t *
svn_io_create_unique_link(const char **unique_name_p,
                          const char *path,
                          const char *dest,
                          const char *suffix,
                          apr_pool_t *pool)
{
#ifdef HAVE_SYMLINK
  unsigned int i;
  const char *unique_name;
  const char *unique_name_apr;
  const char *dest_apr;
  int rv;

  SVN_ERR(cstring_from_utf8(&dest_apr, dest, pool));
  for (i = 1; i <= 99999; i++)
    {
      apr_status_t apr_err;

      /* Special case the first attempt -- if we can avoid having a
         generated numeric portion at all, that's best.  So first we
         try with just the suffix; then future tries add a number
         before the suffix.  (A do-while loop could avoid the repeated
         conditional, but it's not worth the clarity loss.)

         If the first attempt fails, the first number will be "2".
         This is good, since "1" would misleadingly imply that
         the second attempt was actually the first... and if someone's
         got conflicts on their conflicts, we probably don't want to
         add to their confusion :-). */
      if (i == 1)
        unique_name = apr_psprintf(pool, "%s%s", path, suffix);
      else
        unique_name = apr_psprintf(pool, "%s.%u%s", path, i, suffix);

      /* Hmmm.  Ideally, we would append to a native-encoding buf
         before starting iteration, then convert back to UTF-8 for
         return. But I suppose that would make the appending code
         sensitive to i18n in a way it shouldn't be... Oh well. */
      SVN_ERR(cstring_from_utf8(&unique_name_apr, unique_name, pool));
      do {
        rv = symlink(dest_apr, unique_name_apr);
      } while (rv == -1 && APR_STATUS_IS_EINTR(apr_get_os_error()));

      apr_err = apr_get_os_error();

      if (rv == -1 && APR_STATUS_IS_EEXIST(apr_err))
        continue;
      else if (rv == -1 && apr_err)
        {
          /* On Win32, CreateFile fails with an "Access Denied" error
             code, rather than "File Already Exists", if the colliding
             name belongs to a directory. */
          if (APR_STATUS_IS_EACCES(apr_err))
            {
              apr_finfo_t finfo;
              apr_status_t apr_err_2 = apr_stat(&finfo, unique_name_apr,
                                                APR_FINFO_TYPE, pool);

              if (!apr_err_2
                  && (finfo.filetype == APR_DIR))
                continue;

              /* Else ignore apr_err_2; better to fall through and
                 return the original error. */
            }

          *unique_name_p = NULL;
          return svn_error_wrap_apr(apr_err,
                                    _("Can't create symbolic link '%s'"),
                                    svn_dirent_local_style(unique_name, pool));
        }
      else
        {
          *unique_name_p = unique_name;
          return SVN_NO_ERROR;
        }
    }

  *unique_name_p = NULL;
  return svn_error_createf(SVN_ERR_IO_UNIQUE_NAMES_EXHAUSTED,
                           NULL,
                           _("Unable to make name for '%s'"),
                           svn_dirent_local_style(path, pool));
#else
  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                          _("Symbolic links are not supported on this "
                            "platform"));
#endif
}

svn_error_t *
svn_io_read_link(svn_string_t **dest,
                 const char *path,
                 apr_pool_t *pool)
{
#if defined(HAVE_READLINK)
  svn_string_t dest_apr;
  const char *path_apr;
  char buf[1025];
  ssize_t rv;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));
  do {
    rv = readlink(path_apr, buf, sizeof(buf) - 1);
  } while (rv == -1 && APR_STATUS_IS_EINTR(apr_get_os_error()));

  if (rv == -1)
    return svn_error_wrap_apr(apr_get_os_error(),
                              _("Can't read contents of link"));

  buf[rv] = '\0';
  dest_apr.data = buf;
  dest_apr.len = rv;

  /* ### Cast needed, one of these interfaces is wrong */
  return svn_utf_string_to_utf8((const svn_string_t **)dest, &dest_apr, pool);
#elif defined(WIN32)
  return io_win_read_link(dest, path, pool);
#else
  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                          _("Symbolic links are not supported on this "
                            "platform"));
#endif
}


svn_error_t *
svn_io_copy_link(const char *src,
                 const char *dst,
                 apr_pool_t *pool)

{
#ifdef HAVE_READLINK
  svn_string_t *link_dest;
  const char *dst_tmp;

  /* Notice what the link is pointing at... */
  SVN_ERR(svn_io_read_link(&link_dest, src, pool));

  /* Make a tmp-link pointing at the same thing. */
  SVN_ERR(svn_io_create_unique_link(&dst_tmp, dst, link_dest->data,
                                    ".tmp", pool));

  /* Move the tmp-link to link. */
  return svn_io_file_rename(dst_tmp, dst, pool);

#else
  return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                          _("Symbolic links are not supported on this "
                            "platform"));
#endif
}

/* Temporary directory name cache for svn_io_temp_dir() */
static volatile svn_atomic_t temp_dir_init_state = 0;
static const char *temp_dir;

/* Helper function to initialize temp dir. Passed to svn_atomic__init_once */
static svn_error_t *
init_temp_dir(void *baton, apr_pool_t *scratch_pool)
{
  /* Global pool for the temp path */
  apr_pool_t *global_pool = svn_pool_create(NULL);
  const char *dir;

  apr_status_t apr_err = apr_temp_dir_get(&dir, scratch_pool);

  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't find a temporary directory"));

  SVN_ERR(cstring_to_utf8(&dir, dir, scratch_pool));

  dir = svn_dirent_internal_style(dir, scratch_pool);

  SVN_ERR(svn_dirent_get_absolute(&temp_dir, dir, global_pool));

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_temp_dir(const char **dir,
                apr_pool_t *pool)
{
  SVN_ERR(svn_atomic__init_once(&temp_dir_init_state,
                                init_temp_dir, NULL, pool));

  *dir = apr_pstrdup(pool, temp_dir);

  return SVN_NO_ERROR;
}




/*** Creating, copying and appending files. ***/

/* Transfer the contents of FROM_FILE to TO_FILE, using POOL for temporary
 * allocations.
 *
 * NOTE: We don't use apr_copy_file() for this, since it takes filenames
 * as parameters.  Since we want to copy to a temporary file
 * and rename for atomicity (see below), this would require an extra
 * close/open pair, which can be expensive, especially on
 * remote file systems.
 */
static apr_status_t
copy_contents(apr_file_t *from_file,
              apr_file_t *to_file,
              apr_pool_t *pool)
{
  /* Copy bytes till the cows come home. */
  while (1)
    {
      char buf[SVN__STREAM_CHUNK_SIZE];
      apr_size_t bytes_this_time = sizeof(buf);
      apr_status_t read_err;
      apr_status_t write_err;

      /* Read 'em. */
      read_err = apr_file_read(from_file, buf, &bytes_this_time);
      if (read_err && !APR_STATUS_IS_EOF(read_err))
        {
          return read_err;
        }

      /* Write 'em. */
      write_err = apr_file_write_full(to_file, buf, bytes_this_time, NULL);
      if (write_err)
        {
          return write_err;
        }

      if (read_err && APR_STATUS_IS_EOF(read_err))
        {
          /* Return the results of this close: an error, or success. */
          return APR_SUCCESS;
        }
    }
  /* NOTREACHED */
}


svn_error_t *
svn_io_copy_file(const char *src,
                 const char *dst,
                 svn_boolean_t copy_perms,
                 apr_pool_t *pool)
{
  apr_file_t *from_file, *to_file;
  apr_status_t apr_err;
  const char *dst_tmp;
  svn_error_t *err;

  /* ### NOTE: sometimes src == dst. In this case, because we copy to a
     ###   temporary file, and then rename over the top of the destination,
     ###   the net result is resetting the permissions on src/dst.
     ###
     ### Note: specifically, this can happen during a switch when the desired
     ###   permissions for a file change from one branch to another. See
     ###   switch_tests 17.
     ###
     ### ... yes, we should avoid copying to the same file, and we should
     ###     make the "reset perms" explicit. The switch *happens* to work
     ###     because of this copy-to-temp-then-rename implementation. If it
     ###     weren't for that, the switch would break.
  */
#ifdef CHECK_FOR_SAME_FILE
  if (strcmp(src, dst) == 0)
    return SVN_NO_ERROR;
#endif

  SVN_ERR(svn_io_file_open(&from_file, src, APR_READ,
                           APR_OS_DEFAULT, pool));

  /* For atomicity, we copy to a tmp file and then rename the tmp
     file over the real destination. */

  SVN_ERR(svn_io_open_unique_file3(&to_file, &dst_tmp,
                                   svn_dirent_dirname(dst, pool),
                                   svn_io_file_del_none, pool, pool));

  apr_err = copy_contents(from_file, to_file, pool);

  if (apr_err)
    {
      err = svn_error_wrap_apr(apr_err, _("Can't copy '%s' to '%s'"),
                               svn_dirent_local_style(src, pool),
                               svn_dirent_local_style(dst_tmp, pool));
    }
   else
     err = NULL;

  err = svn_error_compose_create(err,
                                 svn_io_file_close(from_file, pool));

  err = svn_error_compose_create(err,
                                 svn_io_file_close(to_file, pool));

  if (err)
    {
      return svn_error_compose_create(
                                 err,
                                 svn_io_remove_file2(dst_tmp, TRUE, pool));
    }

  /* If copying perms, set the perms on dst_tmp now, so they will be
     atomically inherited in the upcoming rename.  But note that we
     had to wait until now to set perms, because if they say
     read-only, then we'd have failed filling dst_tmp's contents. */
  if (copy_perms)
    SVN_ERR(svn_io_copy_perms(src, dst_tmp, pool));

  return svn_error_trace(svn_io_file_rename(dst_tmp, dst, pool));
}

#if !defined(WIN32) && !defined(__OS2__)
/* Wrapper for apr_file_perms_set(), taking a UTF8-encoded filename. */
static svn_error_t *
file_perms_set(const char *fname, apr_fileperms_t perms,
               apr_pool_t *pool)
{
  const char *fname_apr;
  apr_status_t status;

  SVN_ERR(cstring_from_utf8(&fname_apr, fname, pool));

  status = apr_file_perms_set(fname_apr, perms);
  if (status)
    return svn_error_wrap_apr(status, _("Can't set permissions on '%s'"),
                              fname);
  else
    return SVN_NO_ERROR;
}

/* Set permissions PERMS on the FILE. This is a cheaper variant of the
 * file_perms_set wrapper() function because no locale-dependent string
 * conversion is required. POOL will be used for allocations.
 */
static svn_error_t *
file_perms_set2(apr_file_t* file, apr_fileperms_t perms, apr_pool_t *pool)
{
  const char *fname_apr;
  apr_status_t status;

  status = apr_file_name_get(&fname_apr, file);
  if (status)
    return svn_error_wrap_apr(status, _("Can't get file name"));

  status = apr_file_perms_set(fname_apr, perms);
  if (status)
    return svn_error_wrap_apr(status, _("Can't set permissions on '%s'"),
                              try_utf8_from_internal_style(fname_apr, pool));
  else
    return SVN_NO_ERROR;
}

#endif /* !WIN32 && !__OS2__ */

svn_error_t *
svn_io_copy_perms(const char *src,
                  const char *dst,
                  apr_pool_t *pool)
{
  /* ### On Windows or OS/2, apr_file_perms_set always returns APR_ENOTIMPL,
         and the path passed to apr_file_perms_set must be encoded
         in the platform-specific path encoding; not necessary UTF-8.
         We need a platform-specific implementation to get the
         permissions right. */

#if !defined(WIN32) && !defined(__OS2__)
  {
    apr_finfo_t finfo;
    svn_node_kind_t kind;
    svn_boolean_t is_special;
    svn_error_t *err;

    /* If DST is a symlink, don't bother copying permissions. */
    SVN_ERR(svn_io_check_special_path(dst, &kind, &is_special, pool));
    if (is_special)
      return SVN_NO_ERROR;

    SVN_ERR(svn_io_stat(&finfo, src, APR_FINFO_PROT, pool));
    err = file_perms_set(dst, finfo.protection, pool);
    if (err)
      {
        /* We shouldn't be able to get APR_INCOMPLETE or APR_ENOTIMPL
           here under normal circumstances, because the perms themselves
           came from a call to apr_file_info_get(), and we already know
           this is the non-Win32 case.  But if it does happen, it's not
           an error. */
        if (APR_STATUS_IS_INCOMPLETE(err->apr_err) ||
            APR_STATUS_IS_ENOTIMPL(err->apr_err))
          svn_error_clear(err);
        else
          {
            return svn_error_quick_wrapf(
                     err, _("Can't set permissions on '%s'"),
                     svn_dirent_local_style(dst, pool));
          }
      }
  }
#endif /* !WIN32 && !__OS2__ */

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_append_file(const char *src, const char *dst, apr_pool_t *pool)
{
  apr_status_t apr_err;
  const char *src_apr, *dst_apr;

  SVN_ERR(cstring_from_utf8(&src_apr, src, pool));
  SVN_ERR(cstring_from_utf8(&dst_apr, dst, pool));

  apr_err = apr_file_append(src_apr, dst_apr, APR_OS_DEFAULT, pool);

  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't append '%s' to '%s'"),
                              svn_dirent_local_style(src, pool),
                              svn_dirent_local_style(dst, pool));

  return SVN_NO_ERROR;
}


svn_error_t *svn_io_copy_dir_recursively(const char *src,
                                         const char *dst_parent,
                                         const char *dst_basename,
                                         svn_boolean_t copy_perms,
                                         svn_cancel_func_t cancel_func,
                                         void *cancel_baton,
                                         apr_pool_t *pool)
{
  svn_node_kind_t kind;
  apr_status_t status;
  const char *dst_path;
  apr_dir_t *this_dir;
  apr_finfo_t this_entry;
  apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;

  /* Make a subpool for recursion */
  apr_pool_t *subpool = svn_pool_create(pool);

  /* The 'dst_path' is simply dst_parent/dst_basename */
  dst_path = svn_dirent_join(dst_parent, dst_basename, pool);

  /* Sanity checks:  SRC and DST_PARENT are directories, and
     DST_BASENAME doesn't already exist in DST_PARENT. */
  SVN_ERR(svn_io_check_path(src, &kind, subpool));
  if (kind != svn_node_dir)
    return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
                             _("Source '%s' is not a directory"),
                             svn_dirent_local_style(src, pool));

  SVN_ERR(svn_io_check_path(dst_parent, &kind, subpool));
  if (kind != svn_node_dir)
    return svn_error_createf(SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
                             _("Destination '%s' is not a directory"),
                             svn_dirent_local_style(dst_parent, pool));

  SVN_ERR(svn_io_check_path(dst_path, &kind, subpool));
  if (kind != svn_node_none)
    return svn_error_createf(SVN_ERR_ENTRY_EXISTS, NULL,
                             _("Destination '%s' already exists"),
                             svn_dirent_local_style(dst_path, pool));

  /* Create the new directory. */
  /* ### TODO: copy permissions (needs apr_file_attrs_get()) */
  SVN_ERR(svn_io_dir_make(dst_path, APR_OS_DEFAULT, pool));

  /* Loop over the dirents in SRC.  ('.' and '..' are auto-excluded) */
  SVN_ERR(svn_io_dir_open(&this_dir, src, subpool));

  for (status = apr_dir_read(&this_entry, flags, this_dir);
       status == APR_SUCCESS;
       status = apr_dir_read(&this_entry, flags, this_dir))
    {
      if ((this_entry.name[0] == '.')
          && ((this_entry.name[1] == '\0')
              || ((this_entry.name[1] == '.')
                  && (this_entry.name[2] == '\0'))))
        {
          continue;
        }
      else
        {
          const char *src_target, *entryname_utf8;

          if (cancel_func)
            SVN_ERR(cancel_func(cancel_baton));

          SVN_ERR(entry_name_to_utf8(&entryname_utf8, this_entry.name,
                                     src, subpool));
          src_target = svn_dirent_join(src, entryname_utf8, subpool);

          if (this_entry.filetype == APR_REG) /* regular file */
            {
              const char *dst_target = svn_dirent_join(dst_path,
                                                       entryname_utf8,
                                                       subpool);
              SVN_ERR(svn_io_copy_file(src_target, dst_target,
                                       copy_perms, subpool));
            }
          else if (this_entry.filetype == APR_LNK) /* symlink */
            {
              const char *dst_target = svn_dirent_join(dst_path,
                                                       entryname_utf8,
                                                       subpool);
              SVN_ERR(svn_io_copy_link(src_target, dst_target,
                                       subpool));
            }
          else if (this_entry.filetype == APR_DIR) /* recurse */
            {
              /* Prevent infinite recursion by filtering off our
                 newly created destination path. */
              if (strcmp(src, dst_parent) == 0
                  && strcmp(entryname_utf8, dst_basename) == 0)
                continue;

              SVN_ERR(svn_io_copy_dir_recursively
                      (src_target,
                       dst_path,
                       entryname_utf8,
                       copy_perms,
                       cancel_func,
                       cancel_baton,
                       subpool));
            }
          /* ### support other APR node types someday?? */

        }
    }

  if (! (APR_STATUS_IS_ENOENT(status)))
    return svn_error_wrap_apr(status, _("Can't read directory '%s'"),
                              svn_dirent_local_style(src, pool));

  status = apr_dir_close(this_dir);
  if (status)
    return svn_error_wrap_apr(status, _("Error closing directory '%s'"),
                              svn_dirent_local_style(src, pool));

  /* Free any memory used by recursion */
  svn_pool_destroy(subpool);

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_make_dir_recursively(const char *path, apr_pool_t *pool)
{
  const char *path_apr;
  apr_status_t apr_err;

  if (svn_path_is_empty(path))
    /* Empty path (current dir) is assumed to always exist,
       so we do nothing, per docs. */
    return SVN_NO_ERROR;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  apr_err = apr_dir_make_recursive(path_apr, APR_OS_DEFAULT, pool);
#ifdef WIN32
  /* Don't retry on ERROR_ACCESS_DENIED, as that typically signals a
     permanent error */
  if (apr_err == APR_FROM_OS_ERROR(ERROR_SHARING_VIOLATION))
    WIN32_RETRY_LOOP(apr_err, apr_dir_make_recursive(path_apr,
                                                     APR_OS_DEFAULT, pool));
#endif

  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't make directory '%s'"),
                              svn_dirent_local_style(path, pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_file_create_bytes(const char *file,
                         const void *contents,
                         apr_size_t length,
                         apr_pool_t *scratch_pool)
{
  apr_file_t *f;
  apr_size_t written;
  svn_error_t *err = SVN_NO_ERROR;

  SVN_ERR(svn_io_file_open(&f, file,
                           (APR_WRITE | APR_CREATE | APR_EXCL),
                           APR_OS_DEFAULT,
                           scratch_pool));
  if (length)
    err = svn_io_file_write_full(f, contents, length, &written,
                                 scratch_pool);

  err = svn_error_compose_create(
                    err,
                    svn_io_file_close(f, scratch_pool));

  if (err)
    {
      /* Our caller doesn't know if we left a file or not if we return
         an error. Better to cleanup after ourselves if we created the
         file. */
      return svn_error_trace(
                svn_error_compose_create(
                    err,
                    svn_io_remove_file2(file, TRUE, scratch_pool)));
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_file_create(const char *file,
                   const char *contents,
                   apr_pool_t *pool)
{
  return svn_error_trace(svn_io_file_create_bytes(file, contents,
                                                  contents ? strlen(contents)
                                                           : 0,
                                                  pool));
}

svn_error_t *
svn_io_file_create_empty(const char *file,
                         apr_pool_t *scratch_pool)
{
  return svn_error_trace(svn_io_file_create_bytes(file, NULL, 0,
                                                  scratch_pool));
}

svn_error_t *
svn_io_dir_file_copy(const char *src_path,
                     const char *dest_path,
                     const char *file,
                     apr_pool_t *pool)
{
  const char *file_dest_path = svn_dirent_join(dest_path, file, pool);
  const char *file_src_path = svn_dirent_join(src_path, file, pool);

  return svn_error_trace(
            svn_io_copy_file(file_src_path, file_dest_path, TRUE, pool));
}


/*** Modtime checking. ***/

svn_error_t *
svn_io_file_affected_time(apr_time_t *apr_time,
                          const char *path,
                          apr_pool_t *pool)
{
  apr_finfo_t finfo;

  SVN_ERR(svn_io_stat(&finfo, path, APR_FINFO_MIN | APR_FINFO_LINK, pool));

  *apr_time = finfo.mtime;

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_set_file_affected_time(apr_time_t apr_time,
                              const char *path,
                              apr_pool_t *pool)
{
  apr_status_t status;
  const char *native_path;

  SVN_ERR(cstring_from_utf8(&native_path, path, pool));
  status = apr_file_mtime_set(native_path, apr_time, pool);

  if (status)
    return svn_error_wrap_apr(status, _("Can't set access time of '%s'"),
                              svn_dirent_local_style(path, pool));

  return SVN_NO_ERROR;
}


void
svn_io_sleep_for_timestamps(const char *path, apr_pool_t *pool)
{
  apr_time_t now, then;
  svn_error_t *err;
  char *sleep_env_var;

  sleep_env_var = getenv(SVN_SLEEP_ENV_VAR);

  if (sleep_env_var && apr_strnatcasecmp(sleep_env_var, "yes") == 0)
    return; /* Allow skipping for testing */

  now = apr_time_now();

  /* Calculate 0.02 seconds after the next second wallclock tick. */
  then = apr_time_make(apr_time_sec(now) + 1, APR_USEC_PER_SEC / 50);

  /* Worst case is waiting one second, so we can use that time to determine
     if we can sleep shorter than that */
  if (path)
    {
      apr_finfo_t finfo;

      err = svn_io_stat(&finfo, path, APR_FINFO_MTIME | APR_FINFO_LINK, pool);

      if (err)
        {
          svn_error_clear(err); /* Fall back on original behavior */
        }
      else if (finfo.mtime % APR_USEC_PER_SEC)
        {
          /* Very simplistic but safe approach:
              If the filesystem has < sec mtime we can be reasonably sure
              that the filesystem has some sub-second resolution.  On Windows
              it is likely to be sub-millisecond; on Linux systems it depends
              on the filesystem, ext4 is typically 1ms, 4ms or 10ms resolution.

             ## Perhaps find a better algorithm here. This will fail once
                in every 1000 cases on a millisecond precision filesystem
                if the mtime happens to be an exact second.

                But better to fail once in every thousand cases than every
                time, like we did before.

             Note for further research on algorithm:
               FAT32 has < 1 sec precision on ctime, but 2 sec on mtime.

               Linux/ext4 with CONFIG_HZ=250 has high resolution
               apr_time_now and although the filesystem timestamps
               have similar high precision they are only updated with
               a coarser 4ms resolution. */

          /* 10 milliseconds after now. */
#ifndef SVN_HI_RES_SLEEP_MS
#define SVN_HI_RES_SLEEP_MS 10
#endif
          then = now + apr_time_from_msec(SVN_HI_RES_SLEEP_MS);
        }

      /* Remove time taken to do stat() from sleep. */
      now = apr_time_now();
    }

  if (now >= then)
    return; /* Passing negative values may suspend indefinitely (Windows) */

  /* (t < 1000 will be round to 0 in apr) */
  if (then - now < 1000)
    apr_sleep(1000);
  else
    apr_sleep(then - now);
}


svn_error_t *
svn_io_filesizes_different_p(svn_boolean_t *different_p,
                             const char *file1,
                             const char *file2,
                             apr_pool_t *pool)
{
  apr_finfo_t finfo1;
  apr_finfo_t finfo2;
  apr_status_t status;
  const char *file1_apr, *file2_apr;

  /* Not using svn_io_stat() because don't want to generate
     svn_error_t objects for non-error conditions. */

  SVN_ERR(cstring_from_utf8(&file1_apr, file1, pool));
  SVN_ERR(cstring_from_utf8(&file2_apr, file2, pool));

  /* Stat both files */
  status = apr_stat(&finfo1, file1_apr, APR_FINFO_MIN, pool);
  if (status)
    {
      /* If we got an error stat'ing a file, it could be because the
         file was removed... or who knows.  Whatever the case, we
         don't know if the filesizes are definitely different, so
         assume that they're not. */
      *different_p = FALSE;
      return SVN_NO_ERROR;
    }

  status = apr_stat(&finfo2, file2_apr, APR_FINFO_MIN, pool);
  if (status)
    {
      /* See previous comment. */
      *different_p = FALSE;
      return SVN_NO_ERROR;
    }

  /* Examine file sizes */
  if (finfo1.size == finfo2.size)
    *different_p = FALSE;
  else
    *different_p = TRUE;

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_filesizes_three_different_p(svn_boolean_t *different_p12,
                                   svn_boolean_t *different_p23,
                                   svn_boolean_t *different_p13,
                                   const char *file1,
                                   const char *file2,
                                   const char *file3,
                                   apr_pool_t *scratch_pool)
{
  apr_finfo_t finfo1, finfo2, finfo3;
  apr_status_t status1, status2, status3;
  const char *file1_apr, *file2_apr, *file3_apr;

  /* Not using svn_io_stat() because don't want to generate
     svn_error_t objects for non-error conditions. */

  SVN_ERR(cstring_from_utf8(&file1_apr, file1, scratch_pool));
  SVN_ERR(cstring_from_utf8(&file2_apr, file2, scratch_pool));
  SVN_ERR(cstring_from_utf8(&file3_apr, file3, scratch_pool));

  /* Stat all three files */
  status1 = apr_stat(&finfo1, file1_apr, APR_FINFO_MIN, scratch_pool);
  status2 = apr_stat(&finfo2, file2_apr, APR_FINFO_MIN, scratch_pool);
  status3 = apr_stat(&finfo3, file3_apr, APR_FINFO_MIN, scratch_pool);

  /* If we got an error stat'ing a file, it could be because the
     file was removed... or who knows.  Whatever the case, we
     don't know if the filesizes are definitely different, so
     assume that they're not. */
  *different_p12 = !status1 && !status2 && finfo1.size != finfo2.size;
  *different_p23 = !status2 && !status3 && finfo2.size != finfo3.size;
  *different_p13 = !status1 && !status3 && finfo1.size != finfo3.size;

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_file_checksum2(svn_checksum_t **checksum,
                      const char *file,
                      svn_checksum_kind_t kind,
                      apr_pool_t *pool)
{
  svn_stream_t *file_stream;
  svn_stream_t *checksum_stream;
  apr_file_t* f;

  SVN_ERR(svn_io_file_open(&f, file, APR_READ, APR_OS_DEFAULT, pool));
  file_stream = svn_stream_from_aprfile2(f, FALSE, pool);
  checksum_stream = svn_stream_checksummed2(file_stream, checksum, NULL, kind,
                                            TRUE, pool);

  /* Because the checksummed stream will force the reading (and
     checksumming) of all the file's bytes, we can just close the stream
     and let its magic work. */
  return svn_stream_close(checksum_stream);
}


svn_error_t *
svn_io_file_checksum(unsigned char digest[],
                     const char *file,
                     apr_pool_t *pool)
{
  svn_checksum_t *checksum;

  SVN_ERR(svn_io_file_checksum2(&checksum, file, svn_checksum_md5, pool));
  memcpy(digest, checksum->digest, APR_MD5_DIGESTSIZE);

  return SVN_NO_ERROR;
}



/*** Permissions and modes. ***/

#if !defined(WIN32) && !defined(__OS2__)
/* Given the file specified by PATH, attempt to create an
   identical version of it owned by the current user.  This is done by
   moving it to a temporary location, copying the file back to its old
   path, then deleting the temporarily moved version.  All temporary
   allocations are done in POOL. */
static svn_error_t *
reown_file(const char *path,
           apr_pool_t *pool)
{
  const char *unique_name;

  SVN_ERR(svn_io_open_unique_file3(NULL, &unique_name,
                                   svn_dirent_dirname(path, pool),
                                   svn_io_file_del_none, pool, pool));
  SVN_ERR(svn_io_file_rename(path, unique_name, pool));
  SVN_ERR(svn_io_copy_file(unique_name, path, TRUE, pool));
  return svn_error_trace(svn_io_remove_file2(unique_name, FALSE, pool));
}

/* Determine what the PERMS for a new file should be by looking at the
   permissions of a temporary file that we create.
   Unfortunately, umask() as defined in POSIX provides no thread-safe way
   to get at the current value of the umask, so what we're doing here is
   the only way we have to determine which combination of write bits
   (User/Group/World) should be set by default.
   Make temporary allocations in SCRATCH_POOL.  */
static svn_error_t *
get_default_file_perms(apr_fileperms_t *perms, apr_pool_t *scratch_pool)
{
  /* the default permissions as read from the temp folder */
  static apr_fileperms_t default_perms = 0;

  /* Technically, this "racy": Multiple threads may use enter here and
     try to figure out the default permission concurrently. That's fine
     since they will end up with the same results. Even more technical,
     apr_fileperms_t is an atomic type on 32+ bit machines.
   */
  if (default_perms == 0)
    {
      apr_finfo_t finfo;
      apr_file_t *fd;
      const char *fname_base, *fname;
      apr_uint32_t randomish;
      svn_error_t *err;

      /* Get the perms for a newly created file to find out what bits
        should be set.

        Explicitly delete the file because we want this file to be as
        short-lived as possible since its presence means other
        processes may have to try multiple names.

        Using svn_io_open_uniquely_named() here because other tempfile
        creation functions tweak the permission bits of files they create.
      */
      randomish = ((apr_uint32_t)(apr_uintptr_t)scratch_pool
                   + (apr_uint32_t)apr_time_now());
      fname_base = apr_psprintf(scratch_pool, "svn-%08x", randomish);

      SVN_ERR(svn_io_open_uniquely_named(&fd, &fname, NULL, fname_base,
                                         NULL, svn_io_file_del_none,
                                         scratch_pool, scratch_pool));
      err = svn_io_file_info_get(&finfo, APR_FINFO_PROT, fd, scratch_pool);
      err = svn_error_compose_create(err, svn_io_file_close(fd, scratch_pool));
      err = svn_error_compose_create(err, svn_io_remove_file2(fname, TRUE,
                                                              scratch_pool));
      SVN_ERR(err);
      *perms = finfo.protection;
      default_perms = finfo.protection;
    }
  else
    *perms = default_perms;

  return SVN_NO_ERROR;
}

/* OR together permission bits of the file FD and the default permissions
   of a file as determined by get_default_file_perms(). Do temporary
   allocations in SCRATCH_POOL. */
static svn_error_t *
merge_default_file_perms(apr_file_t *fd, apr_fileperms_t *perms,
                         apr_pool_t *scratch_pool)
{
  apr_finfo_t finfo;
  apr_fileperms_t default_perms;

  SVN_ERR(get_default_file_perms(&default_perms, scratch_pool));
  SVN_ERR(svn_io_file_info_get(&finfo, APR_FINFO_PROT, fd, scratch_pool));

  /* Glom the perms together. */
  *perms = default_perms | finfo.protection;
  return SVN_NO_ERROR;
}

/* This is a helper function for the svn_io_set_file_read* functions
   that attempts to honor the users umask when dealing with
   permission changes.  It is a no-op when invoked on a symlink. */
static svn_error_t *
io_set_file_perms(const char *path,
                  svn_boolean_t change_readwrite,
                  svn_boolean_t enable_write,
                  svn_boolean_t change_executable,
                  svn_boolean_t executable,
                  svn_boolean_t ignore_enoent,
                  apr_pool_t *pool)
{
  apr_status_t status;
  const char *path_apr;
  apr_finfo_t finfo;
  apr_fileperms_t perms_to_set;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  /* Try to change only a minimal amount of the perms first
     by getting the current perms and adding bits
     only on where read perms are granted.  If this fails
     fall through to just setting file attributes. */
  status = apr_stat(&finfo, path_apr, APR_FINFO_PROT | APR_FINFO_LINK, pool);
  if (status)
    {
      if (ignore_enoent && (APR_STATUS_IS_ENOENT(status)
                            || SVN__APR_STATUS_IS_ENOTDIR(status)))
        return SVN_NO_ERROR;
      else if (status != APR_ENOTIMPL)
        return svn_error_wrap_apr(status,
                                  _("Can't change perms of file '%s'"),
                                  svn_dirent_local_style(path, pool));
      return SVN_NO_ERROR;
    }

  if (finfo.filetype == APR_LNK)
    return SVN_NO_ERROR;

  perms_to_set = finfo.protection;
  if (change_readwrite)
    {
      if (enable_write) /* Make read-write. */
        {
          /* Tweak the owner bits only. The group/other bits aren't safe to
           * touch because we may end up setting them in undesired ways. */
          perms_to_set |= (APR_UREAD|APR_UWRITE);
        }
      else
        {
          if (finfo.protection & APR_UREAD)
            perms_to_set &= ~APR_UWRITE;
          if (finfo.protection & APR_GREAD)
            perms_to_set &= ~APR_GWRITE;
          if (finfo.protection & APR_WREAD)
            perms_to_set &= ~APR_WWRITE;
        }
    }

  if (change_executable)
    {
      if (executable)
        {
          if (finfo.protection & APR_UREAD)
            perms_to_set |= APR_UEXECUTE;
          if (finfo.protection & APR_GREAD)
            perms_to_set |= APR_GEXECUTE;
          if (finfo.protection & APR_WREAD)
            perms_to_set |= APR_WEXECUTE;
        }
      else
        {
          if (finfo.protection & APR_UREAD)
            perms_to_set &= ~APR_UEXECUTE;
          if (finfo.protection & APR_GREAD)
            perms_to_set &= ~APR_GEXECUTE;
          if (finfo.protection & APR_WREAD)
            perms_to_set &= ~APR_WEXECUTE;
        }
    }

  /* If we aren't changing anything then just return, this saves
     some system calls and helps with shared working copies */
  if (perms_to_set == finfo.protection)
    return SVN_NO_ERROR;

  status = apr_file_perms_set(path_apr, perms_to_set);
  if (!status)
    return SVN_NO_ERROR;

  if (APR_STATUS_IS_EPERM(status))
    {
      /* We don't have permissions to change the
         permissions!  Try a move, copy, and delete
         workaround to see if we can get the file owned by
         us.  If these succeed, try the permissions set
         again.

         Note that we only attempt this in the
         stat-available path.  This assumes that the
         move-copy workaround will only be helpful on
         platforms that implement apr_stat. */
      SVN_ERR(reown_file(path, pool));
      status = apr_file_perms_set(path_apr, perms_to_set);
    }

  if (!status)
    return SVN_NO_ERROR;

  if (ignore_enoent && APR_STATUS_IS_ENOENT(status))
    return SVN_NO_ERROR;
  else if (status == APR_ENOTIMPL)
    {
      /* At least try to set the attributes. */
      apr_fileattrs_t attrs = 0;
      apr_fileattrs_t attrs_values = 0;

      if (change_readwrite)
        {
          attrs = APR_FILE_ATTR_READONLY;
          if (!enable_write)
            attrs_values = APR_FILE_ATTR_READONLY;
        }
      if (change_executable)
        {
          attrs = APR_FILE_ATTR_EXECUTABLE;
          if (executable)
            attrs_values = APR_FILE_ATTR_EXECUTABLE;
        }
      status = apr_file_attrs_set(path_apr, attrs, attrs_values, pool);
    }

  return svn_error_wrap_apr(status,
                            _("Can't change perms of file '%s'"),
                            svn_dirent_local_style(path, pool));
}
#endif /* !WIN32 && !__OS2__ */

#ifdef WIN32
/* This is semantically the same as the APR utf8_to_unicode_path
   function, but reimplemented here because APR does not export it. */
svn_error_t*
svn_io__utf8_to_unicode_longpath(const WCHAR **result,
                                 const char *source,
                                 apr_pool_t *result_pool)
{
    /* This is correct, we don't twist the filename if it will
     * definitely be shorter than 248 characters.  It merits some
     * performance testing to see if this has any effect, but there
     * seem to be applications that get confused by the resulting
     * Unicode \\?\ style file names, especially if they use argv[0]
     * or call the Win32 API functions such as GetModuleName, etc.
     * Not every application is prepared to handle such names.
     *
     * Note also this is shorter than MAX_PATH, as directory paths
     * are actually limited to 248 characters.
     *
     * Note that a utf-8 name can never result in more wide chars
     * than the original number of utf-8 narrow chars.
     */
    const WCHAR *prefix = NULL;
    const int srclen = strlen(source);
    WCHAR *buffer;

    if (srclen > 248)
    {
        if (svn_ctype_isalpha(source[0]) && source[1] == ':'
            && (source[2] == '/' || source[2] == '\\'))
        {
            /* This is an ordinary absolute path. */
            prefix = L"\\\\?\\";
        }
        else if ((source[0] == '/' || source[0] == '\\')
                 && (source[1] == '/' || source[1] == '\\')
                 && source[2] != '?')
        {
            /* This is a UNC path */
            source += 2;        /* Skip the leading slashes */
            prefix = L"\\\\?\\UNC\\";
        }
    }

    SVN_ERR(svn_utf__win32_utf8_to_utf16(&(const WCHAR*)buffer, source,
                                         prefix, result_pool));

    /* Convert slashes to backslashes because the \\?\ path format
       does not allow backslashes as path separators. */
    *result = buffer;
    for (; *buffer; ++buffer)
    {
        if (*buffer == '/')
            *buffer = '\\';
    }
    return SVN_NO_ERROR;
}

/* This is semantically the same as the APR unicode_to_utf8_path
   function, but reimplemented here because APR does not export it. */
static svn_error_t *
io_unicode_to_utf8_path(const char **result,
                        const WCHAR *source,
                        apr_pool_t *result_pool)
{
    const char *utf8_buffer;
    char *buffer;

    SVN_ERR(svn_utf__win32_utf16_to_utf8(&utf8_buffer, source,
                                         NULL, result_pool));
    if (!*utf8_buffer)
      {
        *result = utf8_buffer;
        return SVN_NO_ERROR;
      }

    /* We know that the non-empty buffer returned from the UTF-16 to
       UTF-8 conversion function is in fact writable. */
    buffer = (char*)utf8_buffer;

    /* Skip the leading 4 characters if the path begins \\?\, or substitute
     * // for the \\?\UNC\ path prefix, allocating the maximum string
     * length based on the remaining string, plus the trailing null.
     * then transform \\'s back into /'s since the \\?\ form never
     * allows '/' path separators, and APR always uses '/'s.
     */
    if (0 == strncmp(buffer, "\\\\?\\", 4))
    {
        buffer += 4;
        if (0 == strncmp(buffer, "UNC\\", 4))
        {
            buffer += 2;
            *buffer = '/';
        }
    }

    *result = buffer;
    for (; *buffer; ++buffer)
    {
        if (*buffer == '\\')
            *buffer = '/';
    }
    return SVN_NO_ERROR;
}

static svn_error_t *
io_win_file_attrs_set(const char *fname,
                      DWORD attributes,
                      DWORD attr_mask,
                      apr_pool_t *pool)
{
    /* this is an implementation of apr_file_attrs_set() but one
       that uses the proper Windows attributes instead of the apr
       attributes. This way, we can apply any Windows file and
       folder attributes even if apr doesn't implement them */
    DWORD flags;
    const WCHAR *wfname;

    SVN_ERR(svn_io__utf8_to_unicode_longpath(&wfname, fname, pool));

    flags = GetFileAttributesW(wfname);
    if (flags == 0xFFFFFFFF)
        return svn_error_wrap_apr(apr_get_os_error(),
                                  _("Can't get attributes of file '%s'"),
                                  svn_dirent_local_style(fname, pool));

    flags &= ~attr_mask;
    flags |= (attributes & attr_mask);

    if (!SetFileAttributesW(wfname, flags))
        return svn_error_wrap_apr(apr_get_os_error(),
                                  _("Can't set attributes of file '%s'"),
                                  svn_dirent_local_style(fname, pool));

    return SVN_NO_ERROR;;
}

static svn_error_t *win_init_dynamic_imports(void *baton, apr_pool_t *pool)
{
  HMODULE kernel32 = GetModuleHandleA("kernel32.dll");

  if (kernel32)
    {
      get_final_path_name_by_handle_proc = (GETFINALPATHNAMEBYHANDLE)
        GetProcAddress(kernel32, "GetFinalPathNameByHandleW");

      set_file_information_by_handle_proc = (SetFileInformationByHandle_t)
        GetProcAddress(kernel32, "SetFileInformationByHandle");
    }

  return SVN_NO_ERROR;
}

static svn_error_t * io_win_read_link(svn_string_t **dest,
                                      const char *path,
                                      apr_pool_t *pool)
{
    SVN_ERR(svn_atomic__init_once(&win_dynamic_imports_state,
                                  win_init_dynamic_imports, NULL, pool));

    if (get_final_path_name_by_handle_proc)
      {
        DWORD rv;
        apr_status_t status;
        apr_file_t *file;
        apr_os_file_t filehand;
        WCHAR wdest[APR_PATH_MAX];
        const char *data;

        /* reserve one char for terminating zero. */
        DWORD wdest_len = sizeof(wdest)/sizeof(wdest[0]) - 1;

        status = apr_file_open(&file, path, APR_OPENINFO, APR_OS_DEFAULT, pool);

        if (status)
          return svn_error_wrap_apr(status,
                                    _("Can't read contents of link"));

        apr_os_file_get(&filehand, file);

        rv = get_final_path_name_by_handle_proc(
               filehand, wdest, wdest_len,
               FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);

        /* Save error code. */
        status = apr_get_os_error();

        /* Close file/directory handle in any case. */
        apr_file_close(file);

        /* GetFinaPathNameByHandleW returns number of characters copied to
         * output buffer. Returns zero on error. Returns required buffer size
         * if supplied buffer is not enough. */
        if (rv > wdest_len || rv == 0)
          {
            return svn_error_wrap_apr(status,
                                      _("Can't read contents of link"));
          }

        /* GetFinaPathNameByHandleW doesn't add terminating NUL. */
        wdest[rv] = 0;
        SVN_ERR(io_unicode_to_utf8_path(&data, wdest, pool));

        /* The result is already in the correct pool, so avoid copying
           it to create the string. */
        *dest = svn_string_create_empty(pool);
        if (*data)
          {
            (*dest)->data = data;
            (*dest)->len = strlen(data);
          }

        return SVN_NO_ERROR;
      }
    else
      {
        return svn_error_create(SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                                _("Symbolic links are not supported on this "
                                "platform"));
      }
}

/* Wrapper around Windows API function SetFileInformationByHandle() that
 * returns APR status instead of boolean flag. */
static apr_status_t
win32_set_file_information_by_handle(HANDLE hFile,
                                     int FileInformationClass,
                                     LPVOID lpFileInformation,
                                     DWORD dwBufferSize)
{
  svn_error_clear(svn_atomic__init_once(&win_dynamic_imports_state,
                                        win_init_dynamic_imports,
                                        NULL, NULL));

  if (!set_file_information_by_handle_proc)
    {
      return SVN_ERR_UNSUPPORTED_FEATURE;
    }

  if (!set_file_information_by_handle_proc(hFile, FileInformationClass,
                                           lpFileInformation,
                                           dwBufferSize))
    {
      return apr_get_os_error();
    }

  return APR_SUCCESS;
}

svn_error_t *
svn_io__win_delete_file_on_close(apr_file_t *file,
                                 const char *path,
                                 apr_pool_t *pool)
{
  FILE_DISPOSITION_INFO disposition_info;
  HANDLE hFile;
  apr_status_t status;

  apr_os_file_get(&hFile, file);

  disposition_info.DeleteFile = TRUE;

  status = win32_set_file_information_by_handle(hFile, FileDispositionInfo,
                                                &disposition_info,
                                                sizeof(disposition_info));

  if (status)
    {
      return svn_error_wrap_apr(status, _("Can't remove file '%s'"),
                                svn_dirent_local_style(path, pool));
    }

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io__win_rename_open_file(apr_file_t *file,
                             const char *from_path,
                             const char *to_path,
                             apr_pool_t *pool)
{
  WCHAR *w_final_abspath;
  size_t path_len;
  size_t rename_size;
  FILE_RENAME_INFO *rename_info;
  HANDLE hFile;
  apr_status_t status;

  apr_os_file_get(&hFile, file);

  SVN_ERR(svn_io__utf8_to_unicode_longpath(
            &w_final_abspath, svn_dirent_local_style(to_path,pool),
            pool));

  path_len = wcslen(w_final_abspath);
  rename_size = sizeof(*rename_info) + sizeof(WCHAR) * path_len;

  /* The rename info struct doesn't need hacks for long paths,
     so no ugly escaping calls here */
  rename_info = apr_pcalloc(pool, rename_size);
  rename_info->ReplaceIfExists = TRUE;
  rename_info->FileNameLength = path_len;
  memcpy(rename_info->FileName, w_final_abspath, path_len * sizeof(WCHAR));

  status = win32_set_file_information_by_handle(hFile, FileRenameInfo,
                                                rename_info,
                                                rename_size);

  if (APR_STATUS_IS_EACCES(status) || APR_STATUS_IS_EEXIST(status))
    {
      /* Set the destination file writable because Windows will not allow
         us to rename when final_abspath is read-only. */
      SVN_ERR(svn_io_set_file_read_write(to_path, TRUE, pool));

      status = win32_set_file_information_by_handle(hFile,
                                                    FileRenameInfo,
                                                    rename_info,
                                                    rename_size);
   }

  /* Windows returns Vista+ client accessing network share stored on Windows
     Server 2003 returns ERROR_ACCESS_DENIED. The same happens when Vista+
     client access Windows Server 2008 with disabled SMBv2 protocol.

     So return SVN_ERR_UNSUPPORTED_FEATURE in this case like we do when
     SetFileInformationByHandle() is not available and let caller to
     handle it.

     See "Access denied error on checkout-commit after updating to 1.9.X"
     discussion on dev@s.a.o:
     http://svn.haxx.se/dev/archive-2015-09/0054.shtml */
  if (status == APR_FROM_OS_ERROR(ERROR_ACCESS_DENIED))
    {
      status = SVN_ERR_UNSUPPORTED_FEATURE;
    }

  if (status)
    {
      return svn_error_wrap_apr(status, _("Can't move '%s' to '%s'"),
                                svn_dirent_local_style(from_path, pool),
                                svn_dirent_local_style(to_path, pool));
    }

  return SVN_NO_ERROR;
}

#endif /* WIN32 */

svn_error_t *
svn_io_set_file_read_write_carefully(const char *path,
                                     svn_boolean_t enable_write,
                                     svn_boolean_t ignore_enoent,
                                     apr_pool_t *pool)
{
  if (enable_write)
    return svn_io_set_file_read_write(path, ignore_enoent, pool);
  return svn_io_set_file_read_only(path, ignore_enoent, pool);
}

svn_error_t *
svn_io_set_file_read_only(const char *path,
                          svn_boolean_t ignore_enoent,
                          apr_pool_t *pool)
{
  /* On Windows and OS/2, just set the file attributes -- on unix call
     our internal function which attempts to honor the umask. */
#if !defined(WIN32) && !defined(__OS2__)
  return io_set_file_perms(path, TRUE, FALSE, FALSE, FALSE,
                           ignore_enoent, pool);
#else
  apr_status_t status;
  const char *path_apr;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  status = apr_file_attrs_set(path_apr,
                              APR_FILE_ATTR_READONLY,
                              APR_FILE_ATTR_READONLY,
                              pool);

  if (status && status != APR_ENOTIMPL)
    if (!(ignore_enoent && (APR_STATUS_IS_ENOENT(status)
                            || SVN__APR_STATUS_IS_ENOTDIR(status))))
      return svn_error_wrap_apr(status,
                                _("Can't set file '%s' read-only"),
                                svn_dirent_local_style(path, pool));

  return SVN_NO_ERROR;
#endif
}


svn_error_t *
svn_io_set_file_read_write(const char *path,
                           svn_boolean_t ignore_enoent,
                           apr_pool_t *pool)
{
  /* On Windows and OS/2, just set the file attributes -- on unix call
     our internal function which attempts to honor the umask. */
#if !defined(WIN32) && !defined(__OS2__)
  return io_set_file_perms(path, TRUE, TRUE, FALSE, FALSE,
                           ignore_enoent, pool);
#else
  apr_status_t status;
  const char *path_apr;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  status = apr_file_attrs_set(path_apr,
                              0,
                              APR_FILE_ATTR_READONLY,
                              pool);

  if (status && status != APR_ENOTIMPL)
    if (!ignore_enoent || !APR_STATUS_IS_ENOENT(status))
      return svn_error_wrap_apr(status,
                                _("Can't set file '%s' read-write"),
                                svn_dirent_local_style(path, pool));

  return SVN_NO_ERROR;
#endif
}

svn_error_t *
svn_io_set_file_executable(const char *path,
                           svn_boolean_t executable,
                           svn_boolean_t ignore_enoent,
                           apr_pool_t *pool)
{
  /* On Windows and OS/2, just exit -- on unix call our internal function
  which attempts to honor the umask. */
#if (!defined(WIN32) && !defined(__OS2__))
  return io_set_file_perms(path, FALSE, FALSE, TRUE, executable,
                           ignore_enoent, pool);
#else
  return SVN_NO_ERROR;
#endif
}


svn_error_t *
svn_io__is_finfo_read_only(svn_boolean_t *read_only,
                           apr_finfo_t *file_info,
                           apr_pool_t *pool)
{
#if defined(APR_HAS_USER) && !defined(WIN32) &&!defined(__OS2__)
  apr_status_t apr_err;
  apr_uid_t uid;
  apr_gid_t gid;

  *read_only = FALSE;

  apr_err = apr_uid_current(&uid, &gid, pool);

  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Error getting UID of process"));

  /* Check write bit for current user. */
  if (apr_uid_compare(uid, file_info->user) == APR_SUCCESS)
    *read_only = !(file_info->protection & APR_UWRITE);

  else if (apr_gid_compare(gid, file_info->group) == APR_SUCCESS)
    *read_only = !(file_info->protection & APR_GWRITE);

  else
    *read_only = !(file_info->protection & APR_WWRITE);

#else  /* WIN32 || __OS2__ || !APR_HAS_USER */
  *read_only = (file_info->protection & APR_FREADONLY);
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io__is_finfo_executable(svn_boolean_t *executable,
                            apr_finfo_t *file_info,
                            apr_pool_t *pool)
{
#if defined(APR_HAS_USER) && !defined(WIN32) &&!defined(__OS2__)
  apr_status_t apr_err;
  apr_uid_t uid;
  apr_gid_t gid;

  *executable = FALSE;

  apr_err = apr_uid_current(&uid, &gid, pool);

  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Error getting UID of process"));

  /* Check executable bit for current user. */
  if (apr_uid_compare(uid, file_info->user) == APR_SUCCESS)
    *executable = (file_info->protection & APR_UEXECUTE);

  else if (apr_gid_compare(gid, file_info->group) == APR_SUCCESS)
    *executable = (file_info->protection & APR_GEXECUTE);

  else
    *executable = (file_info->protection & APR_WEXECUTE);

#else  /* WIN32 || __OS2__ || !APR_HAS_USER */
  *executable = FALSE;
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_is_file_executable(svn_boolean_t *executable,
                          const char *path,
                          apr_pool_t *pool)
{
#if defined(APR_HAS_USER) && !defined(WIN32) &&!defined(__OS2__)
  apr_finfo_t file_info;

  SVN_ERR(svn_io_stat(&file_info, path, APR_FINFO_PROT | APR_FINFO_OWNER,
                      pool));
  SVN_ERR(svn_io__is_finfo_executable(executable, &file_info, pool));

#else  /* WIN32 || __OS2__ || !APR_HAS_USER */
  *executable = FALSE;
#endif

  return SVN_NO_ERROR;
}


/*** File locking. ***/
#if !defined(WIN32) && !defined(__OS2__)
/* Clear all outstanding locks on ARG, an open apr_file_t *. */
static apr_status_t
file_clear_locks(void *arg)
{
  apr_status_t apr_err;
  apr_file_t *f = arg;

  /* Remove locks. */
  apr_err = apr_file_unlock(f);
  if (apr_err)
    return apr_err;

  return 0;
}
#endif

svn_error_t *
svn_io_lock_open_file(apr_file_t *lockfile_handle,
                      svn_boolean_t exclusive,
                      svn_boolean_t nonblocking,
                      apr_pool_t *pool)
{
  int locktype = APR_FLOCK_SHARED;
  apr_status_t apr_err;
  const char *fname;

  if (exclusive)
    locktype = APR_FLOCK_EXCLUSIVE;
  if (nonblocking)
    locktype |= APR_FLOCK_NONBLOCK;

  /* We need this only in case of an error but this is cheap to get -
   * so we do it here for clarity. */
  apr_err = apr_file_name_get(&fname, lockfile_handle);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't get file name"));

  /* Get lock on the filehandle. */
  apr_err = apr_file_lock(lockfile_handle, locktype);

  /* In deployments with two or more multithreaded servers running on
     the same system serving two or more fsfs repositories it is
     possible for a deadlock to occur when getting a write lock on
     db/txn-current-lock:

     Process 1                         Process 2
     ---------                         ---------
     thread 1: get lock in repos A
                                       thread 1: get lock in repos B
                                       thread 2: block getting lock in repos A
     thread 2: try to get lock in B *** deadlock ***

     Retry for a while for the deadlock to clear. */
  FILE_LOCK_RETRY_LOOP(apr_err, apr_file_lock(lockfile_handle, locktype));

  if (apr_err)
    {
      switch (locktype & APR_FLOCK_TYPEMASK)
        {
        case APR_FLOCK_SHARED:
          return svn_error_wrap_apr(apr_err,
                                    _("Can't get shared lock on file '%s'"),
                                    try_utf8_from_internal_style(fname, pool));
        case APR_FLOCK_EXCLUSIVE:
          return svn_error_wrap_apr(apr_err,
                                    _("Can't get exclusive lock on file '%s'"),
                                    try_utf8_from_internal_style(fname, pool));
        default:
          SVN_ERR_MALFUNCTION();
        }
    }

/* On Windows and OS/2 file locks are automatically released when
   the file handle closes */
#if !defined(WIN32) && !defined(__OS2__)
  apr_pool_cleanup_register(pool, lockfile_handle,
                            file_clear_locks,
                            apr_pool_cleanup_null);
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_unlock_open_file(apr_file_t *lockfile_handle,
                        apr_pool_t *pool)
{
  const char *fname;
  apr_status_t apr_err;

  /* We need this only in case of an error but this is cheap to get -
   * so we do it here for clarity. */
  apr_err = apr_file_name_get(&fname, lockfile_handle);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't get file name"));

  /* The actual unlock attempt. */
  apr_err = apr_file_unlock(lockfile_handle);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't unlock file '%s'"),
                              try_utf8_from_internal_style(fname, pool));

/* On Windows and OS/2 file locks are automatically released when
   the file handle closes */
#if !defined(WIN32) && !defined(__OS2__)
  apr_pool_cleanup_kill(pool, lockfile_handle, file_clear_locks);
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_file_lock2(const char *lock_file,
                  svn_boolean_t exclusive,
                  svn_boolean_t nonblocking,
                  apr_pool_t *pool)
{
  int locktype = APR_FLOCK_SHARED;
  apr_file_t *lockfile_handle;
  apr_int32_t flags;

  if (exclusive)
    locktype = APR_FLOCK_EXCLUSIVE;

  flags = APR_READ;
  if (locktype == APR_FLOCK_EXCLUSIVE)
    flags |= APR_WRITE;

  /* locktype is never read after this block, so we don't need to bother
     setting it.  If that were to ever change, uncomment the following
     block.
  if (nonblocking)
    locktype |= APR_FLOCK_NONBLOCK;
  */

  SVN_ERR(svn_io_file_open(&lockfile_handle, lock_file, flags,
                           APR_OS_DEFAULT,
                           pool));

  /* Get lock on the filehandle. */
  return svn_io_lock_open_file(lockfile_handle, exclusive, nonblocking, pool);
}

svn_error_t *
svn_io__file_lock_autocreate(const char *lock_file,
                             apr_pool_t *pool)
{
  svn_error_t *err
    = svn_io_file_lock2(lock_file, TRUE, FALSE, pool);
  if (err && APR_STATUS_IS_ENOENT(err->apr_err))
    {
      /* No lock file?  No big deal; these are just empty files anyway.
         Create it and try again. */
      svn_error_clear(err);

      /* This file creation is racy.
         We don't care as long as file gets created at all. */
      err = svn_io_file_create_empty(lock_file, pool);
      if (err && APR_STATUS_IS_EEXIST(err->apr_err))
        {
          svn_error_clear(err);
          err = NULL;
        }

      /* Finally, lock the file - if it exists */
      if (!err)
        err = svn_io_file_lock2(lock_file, TRUE, FALSE, pool);
    }

  return svn_error_trace(err);
}



/* Data consistency/coherency operations. */

svn_error_t *svn_io_file_flush_to_disk(apr_file_t *file,
                                       apr_pool_t *pool)
{
  apr_os_file_t filehand;

  /* ### In apr 1.4+ we could delegate most of this function to
         apr_file_sync(). The only major difference is that this doesn't
         contain the retry loop for EINTR on linux. */

  /* First make sure that any user-space buffered data is flushed. */
  SVN_ERR(svn_io_file_flush(file, pool));

  apr_os_file_get(&filehand, file);

  /* Call the operating system specific function to actually force the
     data to disk. */
  {
#ifdef WIN32

    if (! FlushFileBuffers(filehand))
        return svn_error_wrap_apr(apr_get_os_error(),
                                  _("Can't flush file to disk"));

#else
      int rv;

      do {
#ifdef F_FULLFSYNC
        rv = fcntl(filehand, F_FULLFSYNC, 0);
#else
        rv = fsync(filehand);
#endif
      } while (rv == -1 && APR_STATUS_IS_EINTR(apr_get_os_error()));

      /* If the file is in a memory filesystem, fsync() may return
         EINVAL.  Presumably the user knows the risks, and we can just
         ignore the error. */
      if (rv == -1 && APR_STATUS_IS_EINVAL(apr_get_os_error()))
        return SVN_NO_ERROR;

      if (rv == -1)
        return svn_error_wrap_apr(apr_get_os_error(),
                                  _("Can't flush file to disk"));

#endif
  }
  return SVN_NO_ERROR;
}



/* TODO write test for these two functions, then refactor. */

/* Set RESULT to an svn_stringbuf_t containing the contents of FILE.
   FILENAME is the FILE's on-disk APR-safe name, or NULL if that name
   isn't known.  If CHECK_SIZE is TRUE, the function will attempt to
   first stat() the file to determine it's size before sucking its
   contents into the stringbuf.  (Doing so can prevent unnecessary
   memory usage, an unwanted side effect of the stringbuf growth and
   reallocation mechanism.)  */
static svn_error_t *
stringbuf_from_aprfile(svn_stringbuf_t **result,
                       const char *filename,
                       apr_file_t *file,
                       svn_boolean_t check_size,
                       apr_pool_t *pool)
{
  apr_size_t len;
  svn_error_t *err;
  svn_stringbuf_t *res = NULL;
  apr_size_t res_initial_len = SVN__STREAM_CHUNK_SIZE;
  char *buf;

  /* If our caller wants us to check the size of the file for
     efficient memory handling, we'll try to do so. */
  if (check_size)
    {
      apr_finfo_t finfo = { 0 };

      /* In some cases we get size 0 and no error for non files,
          so we also check for the name. (= cached in apr_file_t) */
      if (! apr_file_info_get(&finfo, APR_FINFO_SIZE, file) && finfo.fname)
        {
          /* we've got the file length. Now, read it in one go. */
          svn_boolean_t eof;
          res_initial_len = (apr_size_t)finfo.size;
          res = svn_stringbuf_create_ensure(res_initial_len, pool);
          SVN_ERR(svn_io_file_read_full2(file, res->data,
                                         res_initial_len, &res->len,
                                         &eof, pool));
          res->data[res->len] = 0;

          *result = res;
          return SVN_NO_ERROR;
        }
    }

  /* XXX: We should check the incoming data for being of type binary. */
  buf = apr_palloc(pool, SVN__STREAM_CHUNK_SIZE);
  res = svn_stringbuf_create_ensure(res_initial_len, pool);

  /* apr_file_read will not return data and eof in the same call. So this loop
   * is safe from missing read data.  */
  len = SVN__STREAM_CHUNK_SIZE;
  err = svn_io_file_read(file, buf, &len, pool);
  while (! err)
    {
      svn_stringbuf_appendbytes(res, buf, len);
      len = SVN__STREAM_CHUNK_SIZE;
      err = svn_io_file_read(file, buf, &len, pool);
    }

  /* Having read all the data we *expect* EOF */
  if (err && !APR_STATUS_IS_EOF(err->apr_err))
    return svn_error_trace(err);
  svn_error_clear(err);

  *result = res;
  return SVN_NO_ERROR;
}

svn_error_t *
svn_stringbuf_from_file2(svn_stringbuf_t **result,
                         const char *filename,
                         apr_pool_t *pool)
{
  apr_file_t *f;

  if (filename[0] == '-' && filename[1] == '\0')
    {
      apr_status_t apr_err;
      if ((apr_err = apr_file_open_stdin(&f, pool)))
        return svn_error_wrap_apr(apr_err, _("Can't open stdin"));
      SVN_ERR(stringbuf_from_aprfile(result, NULL, f, FALSE, pool));
    }
  else
    {
      SVN_ERR(svn_io_file_open(&f, filename, APR_READ, APR_OS_DEFAULT, pool));
      SVN_ERR(stringbuf_from_aprfile(result, filename, f, TRUE, pool));
    }
  return svn_io_file_close(f, pool);
}


svn_error_t *
svn_stringbuf_from_file(svn_stringbuf_t **result,
                        const char *filename,
                        apr_pool_t *pool)
{
  if (filename[0] == '-' && filename[1] == '\0')
    return svn_error_create
        (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
         _("Reading from stdin is disallowed"));
  return svn_stringbuf_from_file2(result, filename, pool);
}

svn_error_t *
svn_stringbuf_from_aprfile(svn_stringbuf_t **result,
                           apr_file_t *file,
                           apr_pool_t *pool)
{
  return stringbuf_from_aprfile(result, NULL, file, TRUE, pool);
}



/* Deletion. */

svn_error_t *
svn_io_remove_file2(const char *path,
                    svn_boolean_t ignore_enoent,
                    apr_pool_t *scratch_pool)
{
  apr_status_t apr_err;
  const char *path_apr;

  SVN_ERR(cstring_from_utf8(&path_apr, path, scratch_pool));

  apr_err = apr_file_remove(path_apr, scratch_pool);

#if defined(WIN32) || defined(__CYGWIN__)
#ifdef WIN32
  /* If the target is read only NTFS reports EACCESS and FAT/FAT32
     reports EEXIST */
  if (APR_STATUS_IS_EACCES(apr_err) || APR_STATUS_IS_EEXIST(apr_err))
    {
      /* Set the destination file writable because Windows will not
         allow us to delete when path is read-only */
      SVN_ERR(svn_io_set_file_read_write(path, ignore_enoent, scratch_pool));
      apr_err = apr_file_remove(path_apr, scratch_pool);

      if (!apr_err)
        return SVN_NO_ERROR;
    }
#endif
  /* Check to make sure we aren't trying to delete a directory */
#ifdef WIN32
  if (apr_err == APR_FROM_OS_ERROR(ERROR_ACCESS_DENIED)
      || apr_err == APR_FROM_OS_ERROR(ERROR_SHARING_VIOLATION))
#else
  if (apr_err == APR_FROM_OS_ERROR(EACCES))
#endif
    {
      apr_finfo_t finfo;

      if (!apr_stat(&finfo, path_apr, APR_FINFO_TYPE, scratch_pool)
          && finfo.filetype == APR_REG)
        {
          WIN32_RETRY_LOOP(apr_err, apr_file_remove(path_apr, scratch_pool));
        }
    }

  /* Just return the delete error */
#endif

  if (!apr_err)
    {
      return SVN_NO_ERROR;
    }
  else if (ignore_enoent && (APR_STATUS_IS_ENOENT(apr_err)
                             || SVN__APR_STATUS_IS_ENOTDIR(apr_err)))
    {
      return SVN_NO_ERROR;
    }
  else
    {
      return svn_error_wrap_apr(apr_err, _("Can't remove file '%s'"),
                                svn_dirent_local_style(path, scratch_pool));
    }
}


svn_error_t *
svn_io_remove_dir(const char *path, apr_pool_t *pool)
{
  return svn_io_remove_dir2(path, FALSE, NULL, NULL, pool);
}

/*
 Mac OS X has a bug where if you're reading the contents of a
 directory via readdir in a loop, and you remove one of the entries in
 the directory and the directory has 338 or more files in it you will
 skip over some of the entries in the directory.  Needless to say,
 this causes problems if you are using this kind of loop inside a
 function that is recursively deleting a directory, because when you
 get around to removing the directory it will still have something in
 it. A similar problem has been observed in other BSDs. This bug has
 since been fixed. See http://www.vnode.ch/fixing_seekdir for details.

 The workaround is to delete the files only _after_ the initial
 directory scan.  A previous workaround involving rewinddir is
 problematic on Win32 and some NFS clients, notably NetBSD.

 See http://subversion.tigris.org/issues/show_bug.cgi?id=1896 and
 http://subversion.tigris.org/issues/show_bug.cgi?id=3501.
*/

/* Neither windows nor unix allows us to delete a non-empty
   directory.

   This is a function to perform the equivalent of 'rm -rf'. */
svn_error_t *
svn_io_remove_dir2(const char *path, svn_boolean_t ignore_enoent,
                   svn_cancel_func_t cancel_func, void *cancel_baton,
                   apr_pool_t *pool)
{
  svn_error_t *err;
  apr_pool_t *subpool;
  apr_hash_t *dirents;
  apr_hash_index_t *hi;

  /* Check for pending cancellation request.
     If we need to bail out, do so early. */

  if (cancel_func)
    SVN_ERR((*cancel_func)(cancel_baton));

  subpool = svn_pool_create(pool);

  err = svn_io_get_dirents3(&dirents, path, TRUE, subpool, subpool);
  if (err)
    {
      /* if the directory doesn't exist, our mission is accomplished */
      if (ignore_enoent && (APR_STATUS_IS_ENOENT(err->apr_err)
                            || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err)))
        {
          svn_error_clear(err);
          return SVN_NO_ERROR;
        }
      return svn_error_trace(err);
    }

  for (hi = apr_hash_first(subpool, dirents); hi; hi = apr_hash_next(hi))
    {
      const char *name = apr_hash_this_key(hi);
      const svn_io_dirent2_t *dirent = apr_hash_this_val(hi);
      const char *fullpath;

      fullpath = svn_dirent_join(path, name, subpool);
      if (dirent->kind == svn_node_dir)
        {
          /* Don't check for cancellation, the callee will immediately do so */
          SVN_ERR(svn_io_remove_dir2(fullpath, FALSE, cancel_func,
                                     cancel_baton, subpool));
        }
      else
        {
          if (cancel_func)
            SVN_ERR((*cancel_func)(cancel_baton));

          err = svn_io_remove_file2(fullpath, FALSE, subpool);
          if (err)
            return svn_error_createf
              (err->apr_err, err, _("Can't remove '%s'"),
               svn_dirent_local_style(fullpath, subpool));
        }
    }

  svn_pool_destroy(subpool);

  return svn_io_dir_remove_nonrecursive(path, pool);
}

svn_error_t *
svn_io_get_dir_filenames(apr_hash_t **dirents,
                         const char *path,
                         apr_pool_t *pool)
{
  return svn_error_trace(svn_io_get_dirents3(dirents, path, TRUE,
                                             pool, pool));
}

svn_io_dirent2_t *
svn_io_dirent2_create(apr_pool_t *result_pool)
{
  svn_io_dirent2_t *dirent = apr_pcalloc(result_pool, sizeof(*dirent));

  /*dirent->kind = svn_node_none;
  dirent->special = FALSE;*/
  dirent->filesize = SVN_INVALID_FILESIZE;
  /*dirent->mtime = 0;*/

  return dirent;
}

svn_io_dirent2_t *
svn_io_dirent2_dup(const svn_io_dirent2_t *item,
                   apr_pool_t *result_pool)
{
  return apr_pmemdup(result_pool,
                     item,
                     sizeof(*item));
}

svn_error_t *
svn_io_get_dirents3(apr_hash_t **dirents,
                    const char *path,
                    svn_boolean_t only_check_type,
                    apr_pool_t *result_pool,
                    apr_pool_t *scratch_pool)
{
  apr_status_t status;
  apr_dir_t *this_dir;
  apr_finfo_t this_entry;
  apr_int32_t flags = APR_FINFO_TYPE | APR_FINFO_NAME;

  if (!only_check_type)
    flags |= APR_FINFO_SIZE | APR_FINFO_MTIME;

  *dirents = apr_hash_make(result_pool);

  SVN_ERR(svn_io_dir_open(&this_dir, path, scratch_pool));

  for (status = apr_dir_read(&this_entry, flags, this_dir);
       status == APR_SUCCESS;
       status = apr_dir_read(&this_entry, flags, this_dir))
    {
      if ((this_entry.name[0] == '.')
          && ((this_entry.name[1] == '\0')
              || ((this_entry.name[1] == '.')
                  && (this_entry.name[2] == '\0'))))
        {
          continue;
        }
      else
        {
          const char *name;
          svn_io_dirent2_t *dirent = svn_io_dirent2_create(result_pool);

          SVN_ERR(entry_name_to_utf8(&name, this_entry.name, path, result_pool));

          map_apr_finfo_to_node_kind(&(dirent->kind),
                                     &(dirent->special),
                                     &this_entry);

          if (!only_check_type)
            {
              dirent->filesize = this_entry.size;
              dirent->mtime = this_entry.mtime;
            }

          svn_hash_sets(*dirents, name, dirent);
        }
    }

  if (! (APR_STATUS_IS_ENOENT(status)))
    return svn_error_wrap_apr(status, _("Can't read directory '%s'"),
                              svn_dirent_local_style(path, scratch_pool));

  status = apr_dir_close(this_dir);
  if (status)
    return svn_error_wrap_apr(status, _("Error closing directory '%s'"),
                              svn_dirent_local_style(path, scratch_pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_stat_dirent2(const svn_io_dirent2_t **dirent_p,
                    const char *path,
                    svn_boolean_t verify_truename,
                    svn_boolean_t ignore_enoent,
                    apr_pool_t *result_pool,
                    apr_pool_t *scratch_pool)
{
  apr_finfo_t finfo;
  svn_io_dirent2_t *dirent;
  svn_error_t *err;
  apr_int32_t wanted = APR_FINFO_TYPE | APR_FINFO_LINK
                       | APR_FINFO_SIZE | APR_FINFO_MTIME;

#if defined(WIN32) || defined(__OS2__)
  if (verify_truename)
    wanted |= APR_FINFO_NAME;
#endif

  err = svn_io_stat(&finfo, path, wanted, scratch_pool);

  if (err && ignore_enoent &&
      (APR_STATUS_IS_ENOENT(err->apr_err)
       || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err)))
    {
      svn_error_clear(err);
      dirent = svn_io_dirent2_create(result_pool);
      SVN_ERR_ASSERT(dirent->kind == svn_node_none);

      *dirent_p = dirent;
      return SVN_NO_ERROR;
    }
  SVN_ERR(err);

#if defined(WIN32) || defined(__OS2__) || defined(DARWIN)
  if (verify_truename)
    {
      const char *requested_name = svn_dirent_basename(path, NULL);

      if (requested_name[0] == '\0')
        {
          /* No parent directory. No need to stat/verify */
        }
#if defined(WIN32) || defined(__OS2__)
      else if (finfo.name)
        {
          const char *name_on_disk;
          SVN_ERR(entry_name_to_utf8(&name_on_disk, finfo.name, path,
                                     scratch_pool));

          if (strcmp(name_on_disk, requested_name) /* != 0 */)
            {
              if (ignore_enoent)
                {
                  *dirent_p = svn_io_dirent2_create(result_pool);
                  return SVN_NO_ERROR;
                }
              else
                return svn_error_createf(APR_ENOENT, NULL,
                          _("Path '%s' not found, case obstructed by '%s'"),
                          svn_dirent_local_style(path, scratch_pool),
                          name_on_disk);
            }
        }
#elif defined(DARWIN)
      /* Currently apr doesn't set finfo.name on DARWIN, returning
                   APR_INCOMPLETE.
         ### Can we optimize this in another way? */
      else
        {
          apr_hash_t *dirents;

          err = svn_io_get_dirents3(&dirents,
                                    svn_dirent_dirname(path, scratch_pool),
                                    TRUE /* only_check_type */,
                                    scratch_pool, scratch_pool);

          if (err && ignore_enoent
              && (APR_STATUS_IS_ENOENT(err->apr_err)
                  || SVN__APR_STATUS_IS_ENOTDIR(err->apr_err)))
            {
              svn_error_clear(err);

              *dirent_p = svn_io_dirent2_create(result_pool);
              return SVN_NO_ERROR;
            }
          else
            SVN_ERR(err);

          if (! svn_hash_gets(dirents, requested_name))
            {
              if (ignore_enoent)
                {
                  *dirent_p = svn_io_dirent2_create(result_pool);
                  return SVN_NO_ERROR;
                }
              else
                return svn_error_createf(APR_ENOENT, NULL,
                          _("Path '%s' not found"),
                          svn_dirent_local_style(path, scratch_pool));
            }
        }
#endif
    }
#endif

  dirent = svn_io_dirent2_create(result_pool);
  map_apr_finfo_to_node_kind(&(dirent->kind), &(dirent->special), &finfo);

  dirent->filesize = finfo.size;
  dirent->mtime = finfo.mtime;

  *dirent_p = dirent;

  return SVN_NO_ERROR;
}

/* Pool userdata key for the error file passed to svn_io_start_cmd(). */
#define ERRFILE_KEY "svn-io-start-cmd-errfile"

/* Handle an error from the child process (before command execution) by
   printing DESC and the error string corresponding to STATUS to stderr. */
static void
handle_child_process_error(apr_pool_t *pool, apr_status_t status,
                           const char *desc)
{
  char errbuf[256];
  apr_file_t *errfile;
  void *p;

  /* We can't do anything if we get an error here, so just return. */
  if (apr_pool_userdata_get(&p, ERRFILE_KEY, pool))
    return;
  errfile = p;

  if (errfile)
    /* What we get from APR is in native encoding. */
    apr_file_printf(errfile, "%s: %s",
                    desc, apr_strerror(status, errbuf,
                                       sizeof(errbuf)));
}


svn_error_t *
svn_io_start_cmd3(apr_proc_t *cmd_proc,
                  const char *path,
                  const char *cmd,
                  const char *const *args,
                  const char *const *env,
                  svn_boolean_t inherit,
                  svn_boolean_t infile_pipe,
                  apr_file_t *infile,
                  svn_boolean_t outfile_pipe,
                  apr_file_t *outfile,
                  svn_boolean_t errfile_pipe,
                  apr_file_t *errfile,
                  apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_procattr_t *cmdproc_attr;
  int num_args;
  const char **args_native;
  const char *cmd_apr;

  SVN_ERR_ASSERT(!((infile != NULL) && infile_pipe));
  SVN_ERR_ASSERT(!((outfile != NULL) && outfile_pipe));
  SVN_ERR_ASSERT(!((errfile != NULL) && errfile_pipe));

  /* Create the process attributes. */
  apr_err = apr_procattr_create(&cmdproc_attr, pool);
  if (apr_err)
    return svn_error_wrap_apr(apr_err,
                              _("Can't create process '%s' attributes"),
                              cmd);

  /* Make sure we invoke cmd directly, not through a shell. */
  apr_err = apr_procattr_cmdtype_set(cmdproc_attr,
                                     inherit ? APR_PROGRAM_PATH : APR_PROGRAM);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't set process '%s' cmdtype"),
                              cmd);

  /* Set the process's working directory. */
  if (path)
    {
      const char *path_apr;

      /* APR doesn't like our canonical path format for current directory */
      if (path[0] == '\0')
        path = ".";

      SVN_ERR(cstring_from_utf8(&path_apr, path, pool));
      apr_err = apr_procattr_dir_set(cmdproc_attr, path_apr);
      if (apr_err)
        return svn_error_wrap_apr(apr_err,
                                  _("Can't set process '%s' directory"),
                                  cmd);
    }

  /* Use requested inputs and outputs.

     ### Unfortunately each of these apr functions creates a pipe and then
     overwrites the pipe file descriptor with the descriptor we pass
     in. The pipes can then never be closed. This is an APR bug. */
  if (infile)
    {
      apr_err = apr_procattr_child_in_set(cmdproc_attr, infile, NULL);
      if (apr_err)
        return svn_error_wrap_apr(apr_err,
                                  _("Can't set process '%s' child input"),
                                  cmd);
    }
  if (outfile)
    {
      apr_err = apr_procattr_child_out_set(cmdproc_attr, outfile, NULL);
      if (apr_err)
        return svn_error_wrap_apr(apr_err,
                                  _("Can't set process '%s' child outfile"),
                                  cmd);
    }
  if (errfile)
    {
      apr_err = apr_procattr_child_err_set(cmdproc_attr, errfile, NULL);
      if (apr_err)
        return svn_error_wrap_apr(apr_err,
                                  _("Can't set process '%s' child errfile"),
                                  cmd);
    }

  /* Forward request for pipes to APR. */
  if (infile_pipe || outfile_pipe || errfile_pipe)
    {
      apr_err = apr_procattr_io_set(cmdproc_attr,
                                    infile_pipe ? APR_FULL_BLOCK : APR_NO_PIPE,
                                    outfile_pipe ? APR_FULL_BLOCK : APR_NO_PIPE,
                                    errfile_pipe ? APR_FULL_BLOCK : APR_NO_PIPE);

      if (apr_err)
        return svn_error_wrap_apr(apr_err,
                                  _("Can't set process '%s' stdio pipes"),
                                  cmd);
    }

  /* Have the child print any problems executing its program to errfile. */
  apr_err = apr_pool_userdata_set(errfile, ERRFILE_KEY, NULL, pool);
  if (apr_err)
    return svn_error_wrap_apr(apr_err,
                              _("Can't set process '%s' child errfile for "
                                "error handler"),
                              cmd);
  apr_err = apr_procattr_child_errfn_set(cmdproc_attr,
                                         handle_child_process_error);
  if (apr_err)
    return svn_error_wrap_apr(apr_err,
                              _("Can't set process '%s' error handler"),
                              cmd);

  /* Convert cmd and args from UTF-8 */
  SVN_ERR(cstring_from_utf8(&cmd_apr, cmd, pool));
  for (num_args = 0; args[num_args]; num_args++)
    ;
  args_native = apr_palloc(pool, (num_args + 1) * sizeof(char *));
  args_native[num_args] = NULL;
  while (num_args--)
    {
      /* ### Well, it turns out that on APR on Windows expects all
             program args to be in UTF-8. Callers of svn_io_run_cmd
             should be aware of that. */
      SVN_ERR(cstring_from_utf8(&args_native[num_args],
                                args[num_args], pool));
    }


  /* Start the cmd command. */
  apr_err = apr_proc_create(cmd_proc, cmd_apr, args_native,
                            inherit ? NULL : env, cmdproc_attr, pool);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't start process '%s'"), cmd);

  return SVN_NO_ERROR;
}

#undef ERRFILE_KEY

svn_error_t *
svn_io_wait_for_cmd(apr_proc_t *cmd_proc,
                    const char *cmd,
                    int *exitcode,
                    apr_exit_why_e *exitwhy,
                    apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_exit_why_e exitwhy_val;
  int exitcode_val;

  /* The Win32 apr_proc_wait doesn't set this... */
  exitwhy_val = APR_PROC_EXIT;

  /* Wait for the cmd command to finish. */
  apr_err = apr_proc_wait(cmd_proc, &exitcode_val, &exitwhy_val, APR_WAIT);
  if (!APR_STATUS_IS_CHILD_DONE(apr_err))
    return svn_error_wrap_apr(apr_err, _("Error waiting for process '%s'"),
                              cmd);

  if (exitwhy)
    *exitwhy = exitwhy_val;
  else if (APR_PROC_CHECK_SIGNALED(exitwhy_val)
           && APR_PROC_CHECK_CORE_DUMP(exitwhy_val))
    return svn_error_createf
      (SVN_ERR_EXTERNAL_PROGRAM, NULL,
       _("Process '%s' failed (signal %d, core dumped)"),
       cmd, exitcode_val);
  else if (APR_PROC_CHECK_SIGNALED(exitwhy_val))
    return svn_error_createf
      (SVN_ERR_EXTERNAL_PROGRAM, NULL,
       _("Process '%s' failed (signal %d)"),
       cmd, exitcode_val);
  else if (! APR_PROC_CHECK_EXIT(exitwhy_val))
    /* Don't really know what happened here. */
    return svn_error_createf
      (SVN_ERR_EXTERNAL_PROGRAM, NULL,
       _("Process '%s' failed (exitwhy %d, exitcode %d)"),
       cmd, exitwhy_val, exitcode_val);

  if (exitcode)
    *exitcode = exitcode_val;
  else if (exitcode_val != 0)
    return svn_error_createf
      (SVN_ERR_EXTERNAL_PROGRAM, NULL,
       _("Process '%s' returned error exitcode %d"), cmd, exitcode_val);

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_run_cmd(const char *path,
               const char *cmd,
               const char *const *args,
               int *exitcode,
               apr_exit_why_e *exitwhy,
               svn_boolean_t inherit,
               apr_file_t *infile,
               apr_file_t *outfile,
               apr_file_t *errfile,
               apr_pool_t *pool)
{
  apr_proc_t cmd_proc;

  SVN_ERR(svn_io_start_cmd3(&cmd_proc, path, cmd, args, NULL, inherit,
                            FALSE, infile, FALSE, outfile, FALSE, errfile,
                            pool));

  return svn_io_wait_for_cmd(&cmd_proc, cmd, exitcode, exitwhy, pool);
}


svn_error_t *
svn_io_run_diff2(const char *dir,
                 const char *const *user_args,
                 int num_user_args,
                 const char *label1,
                 const char *label2,
                 const char *from,
                 const char *to,
                 int *pexitcode,
                 apr_file_t *outfile,
                 apr_file_t *errfile,
                 const char *diff_cmd,
                 apr_pool_t *pool)
{
  const char **args;
  int i;
  int exitcode;
  int nargs = 4; /* the diff command itself, two paths, plus a trailing NULL */
  apr_pool_t *subpool = svn_pool_create(pool);

  if (pexitcode == NULL)
    pexitcode = &exitcode;

  if (user_args != NULL)
    nargs += num_user_args;
  else
    nargs += 1; /* -u */

  if (label1 != NULL)
    nargs += 2; /* the -L and the label itself */
  if (label2 != NULL)
    nargs += 2; /* the -L and the label itself */

  args = apr_palloc(subpool, nargs * sizeof(char *));

  i = 0;
  args[i++] = diff_cmd;

  if (user_args != NULL)
    {
      int j;
      for (j = 0; j < num_user_args; ++j)
        args[i++] = user_args[j];
    }
  else
    args[i++] = "-u"; /* assume -u if the user didn't give us any args */

  if (label1 != NULL)
    {
      args[i++] = "-L";
      args[i++] = label1;
    }
  if (label2 != NULL)
    {
      args[i++] = "-L";
      args[i++] = label2;
    }

  args[i++] = svn_dirent_local_style(from, subpool);
  args[i++] = svn_dirent_local_style(to, subpool);
  args[i++] = NULL;

  SVN_ERR_ASSERT(i == nargs);

  SVN_ERR(svn_io_run_cmd(dir, diff_cmd, args, pexitcode, NULL, TRUE,
                         NULL, outfile, errfile, subpool));

  /* The man page for (GNU) diff describes the return value as:

       "An exit status of 0 means no differences were found, 1 means
        some differences were found, and 2 means trouble."

     A return value of 2 typically occurs when diff cannot read its input
     or write to its output, but in any case we probably ought to return an
     error for anything other than 0 or 1 as the output is likely to be
     corrupt.
   */
  if (*pexitcode != 0 && *pexitcode != 1)
    return svn_error_createf(SVN_ERR_EXTERNAL_PROGRAM, NULL,
                             _("'%s' returned %d"),
                             svn_dirent_local_style(diff_cmd, pool),
                             *pexitcode);

  svn_pool_destroy(subpool);

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_run_diff3_3(int *exitcode,
                   const char *dir,
                   const char *mine,
                   const char *older,
                   const char *yours,
                   const char *mine_label,
                   const char *older_label,
                   const char *yours_label,
                   apr_file_t *merged,
                   const char *diff3_cmd,
                   const apr_array_header_t *user_args,
                   apr_pool_t *pool)
{
  const char **args = apr_palloc(pool,
                                 sizeof(char*) * (13
                                                  + (user_args
                                                     ? user_args->nelts
                                                     : 1)));
#ifndef NDEBUG
  int nargs = 12;
#endif
  int i = 0;

  /* Labels fall back to sensible defaults if not specified. */
  if (mine_label == NULL)
    mine_label = ".working";
  if (older_label == NULL)
    older_label = ".old";
  if (yours_label == NULL)
    yours_label = ".new";

  /* Set up diff3 command line. */
  args[i++] = diff3_cmd;
  if (user_args)
    {
      int j;
      for (j = 0; j < user_args->nelts; ++j)
        args[i++] = APR_ARRAY_IDX(user_args, j, const char *);
#ifndef NDEBUG
      nargs += user_args->nelts;
#endif
    }
  else
    {
      args[i++] = "-E";             /* We tried "-A" here, but that caused
                                       overlapping identical changes to
                                       conflict.  See issue #682. */
#ifndef NDEBUG
      ++nargs;
#endif
    }
  args[i++] = "-m";
  args[i++] = "-L";
  args[i++] = mine_label;
  args[i++] = "-L";
  args[i++] = older_label;      /* note:  this label is ignored if
                                   using 2-part markers, which is the
                                   case with "-E". */
  args[i++] = "-L";
  args[i++] = yours_label;
#ifdef SVN_DIFF3_HAS_DIFF_PROGRAM_ARG
  {
    svn_boolean_t has_arg;

    /* ### FIXME: we really shouldn't be reading the config here;
       instead, the necessary bits should be passed in by the caller.
       But should we add another parameter to this function, when the
       whole external diff3 thing might eventually go away?  */
    apr_hash_t *config;
    svn_config_t *cfg;

    SVN_ERR(svn_config_get_config(&config, pool));
    cfg = config ? svn_hash_gets(config, SVN_CONFIG_CATEGORY_CONFIG) : NULL;
    SVN_ERR(svn_config_get_bool(cfg, &has_arg, SVN_CONFIG_SECTION_HELPERS,
                                SVN_CONFIG_OPTION_DIFF3_HAS_PROGRAM_ARG,
                                TRUE));
    if (has_arg)
      {
        const char *diff_cmd, *diff_utf8;
        svn_config_get(cfg, &diff_cmd, SVN_CONFIG_SECTION_HELPERS,
                       SVN_CONFIG_OPTION_DIFF_CMD, SVN_CLIENT_DIFF);
        SVN_ERR(cstring_to_utf8(&diff_utf8, diff_cmd, pool));
        args[i++] = apr_pstrcat(pool, "--diff-program=", diff_utf8,
                                SVN_VA_NULL);
#ifndef NDEBUG
        ++nargs;
#endif
      }
  }
#endif
  args[i++] = svn_dirent_local_style(mine, pool);
  args[i++] = svn_dirent_local_style(older, pool);
  args[i++] = svn_dirent_local_style(yours, pool);
  args[i++] = NULL;
#ifndef NDEBUG
  SVN_ERR_ASSERT(i == nargs);
#endif

  /* Run diff3, output the merged text into the scratch file. */
  SVN_ERR(svn_io_run_cmd(dir, diff3_cmd, args,
                         exitcode, NULL,
                         TRUE, /* keep environment */
                         NULL, merged, NULL,
                         pool));

  /* According to the diff3 docs, a '0' means the merge was clean, and
     '1' means conflict markers were found.  Anything else is real
     error. */
  if ((*exitcode != 0) && (*exitcode != 1))
    return svn_error_createf(SVN_ERR_EXTERNAL_PROGRAM, NULL,
                             _("Error running '%s':  exitcode was %d, "
                               "args were:"
                               "\nin directory '%s', basenames:\n%s\n%s\n%s"),
                             svn_dirent_local_style(diff3_cmd, pool),
                             *exitcode,
                             svn_dirent_local_style(dir, pool),
                             /* Don't call svn_path_local_style() on
                                the basenames.  We don't want them to
                                be absolute, and we don't need the
                                separator conversion. */
                             mine, older, yours);

  return SVN_NO_ERROR;
}


/* Canonicalize a string for hashing.  Modifies KEY in place. */
static APR_INLINE char *
fileext_tolower(char *key)
{
  register char *p;
  for (p = key; *p != 0; ++p)
    *p = (char)apr_tolower(*p);
  return key;
}


svn_error_t *
svn_io_parse_mimetypes_file(apr_hash_t **type_map,
                            const char *mimetypes_file,
                            apr_pool_t *pool)
{
  svn_error_t *err = SVN_NO_ERROR;
  apr_hash_t *types = apr_hash_make(pool);
  svn_boolean_t eof = FALSE;
  svn_stringbuf_t *buf;
  apr_pool_t *subpool = svn_pool_create(pool);
  apr_file_t *types_file;
  svn_stream_t *mimetypes_stream;

  SVN_ERR(svn_io_file_open(&types_file, mimetypes_file,
                           APR_READ, APR_OS_DEFAULT, pool));
  mimetypes_stream = svn_stream_from_aprfile2(types_file, FALSE, pool);

  while (1)
    {
      apr_array_header_t *tokens;
      const char *type;

      svn_pool_clear(subpool);

      /* Read a line. */
      if ((err = svn_stream_readline(mimetypes_stream, &buf,
                                     APR_EOL_STR, &eof, subpool)))
        break;

      /* Only pay attention to non-empty, non-comment lines. */
      if (buf->len)
        {
          int i;

          if (buf->data[0] == '#')
            continue;

          /* Tokenize (into our return pool). */
          tokens = svn_cstring_split(buf->data, " \t", TRUE, pool);
          if (tokens->nelts < 2)
            continue;

          /* The first token in a multi-token line is the media type.
             Subsequent tokens are filename extensions associated with
             that media type. */
          type = APR_ARRAY_IDX(tokens, 0, const char *);
          for (i = 1; i < tokens->nelts; i++)
            {
              /* We can safely address 'ext' as a non-const string because
               * we know svn_cstring_split() allocated it in 'pool' for us. */
              char *ext = APR_ARRAY_IDX(tokens, i, char *);
              fileext_tolower(ext);
              svn_hash_sets(types, ext, type);
            }
        }
      if (eof)
        break;
    }
  svn_pool_destroy(subpool);

  /* If there was an error above, close the file (ignoring any error
     from *that*) and return the originally error. */
  if (err)
    {
      svn_error_clear(svn_stream_close(mimetypes_stream));
      return err;
    }

  /* Close the stream (which closes the underlying file, too). */
  SVN_ERR(svn_stream_close(mimetypes_stream));

  *type_map = types;
  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_detect_mimetype2(const char **mimetype,
                        const char *file,
                        apr_hash_t *mimetype_map,
                        apr_pool_t *pool)
{
  static const char * const generic_binary = "application/octet-stream";

  svn_node_kind_t kind;
  apr_file_t *fh;
  svn_error_t *err;
  unsigned char block[1024];
  apr_size_t amt_read = sizeof(block);

  /* Default return value is NULL. */
  *mimetype = NULL;

  /* If there is a mimetype_map provided, we'll first try to look up
     our file's extension in the map.  Failing that, we'll run the
     heuristic. */
  if (mimetype_map)
    {
      const char *type_from_map;
      char *path_ext; /* Can point to physical const memory but only when
                         svn_path_splitext sets it to "". */
      svn_path_splitext(NULL, (const char **)&path_ext, file, pool);
      fileext_tolower(path_ext);
      if ((type_from_map = svn_hash_gets(mimetype_map, path_ext)))
        {
          *mimetype = type_from_map;
          return SVN_NO_ERROR;
        }
    }

  /* See if this file even exists, and make sure it really is a file. */
  SVN_ERR(svn_io_check_path(file, &kind, pool));
  if (kind != svn_node_file)
    return svn_error_createf(SVN_ERR_BAD_FILENAME, NULL,
                             _("Can't detect MIME type of non-file '%s'"),
                             svn_dirent_local_style(file, pool));

  SVN_ERR(svn_io_file_open(&fh, file, APR_READ, 0, pool));

  /* Read a block of data from FILE. */
  err = svn_io_file_read(fh, block, &amt_read, pool);
  if (err && ! APR_STATUS_IS_EOF(err->apr_err))
    return err;
  svn_error_clear(err);

  /* Now close the file.  No use keeping it open any more.  */
  SVN_ERR(svn_io_file_close(fh, pool));

  if (svn_io_is_binary_data(block, amt_read))
    *mimetype = generic_binary;

  return SVN_NO_ERROR;
}


svn_boolean_t
svn_io_is_binary_data(const void *data, apr_size_t len)
{
  const unsigned char *buf = data;

  if (len == 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF)
    {
      /* This is an empty UTF-8 file which only contains the UTF-8 BOM.
       * Treat it as plain text. */
      return FALSE;
    }

  /* Right now, this function is going to be really stupid.  It's
     going to examine the block of data, and make sure that 15%
     of the bytes are such that their value is in the ranges 0x07-0x0D
     or 0x20-0x7F, and that none of those bytes is 0x00.  If those
     criteria are not met, we're calling it binary.

     NOTE:  Originally, I intended to target 85% of the bytes being in
     the specified ranges, but I flubbed the condition.  At any rate,
     folks aren't complaining, so I'm not sure that it's worth
     adjusting this retroactively now.  --cmpilato  */
  if (len > 0)
    {
      apr_size_t i;
      apr_size_t binary_count = 0;

      /* Run through the data we've read, counting the 'binary-ish'
         bytes.  HINT: If we see a 0x00 byte, we'll set our count to its
         max and stop reading the file. */
      for (i = 0; i < len; i++)
        {
          if (buf[i] == 0)
            {
              binary_count = len;
              break;
            }
          if ((buf[i] < 0x07)
              || ((buf[i] > 0x0D) && (buf[i] < 0x20))
              || (buf[i] > 0x7F))
            {
              binary_count++;
            }
        }

      return (((binary_count * 1000) / len) > 850);
    }

  return FALSE;
}


svn_error_t *
svn_io_detect_mimetype(const char **mimetype,
                       const char *file,
                       apr_pool_t *pool)
{
  return svn_io_detect_mimetype2(mimetype, file, NULL, pool);
}


svn_error_t *
svn_io_file_open(apr_file_t **new_file, const char *fname,
                 apr_int32_t flag, apr_fileperms_t perm,
                 apr_pool_t *pool)
{
  const char *fname_apr;
  apr_status_t status;

  SVN_ERR(cstring_from_utf8(&fname_apr, fname, pool));
  status = file_open(new_file, fname_apr, flag | APR_BINARY, perm, TRUE,
                     pool);

  if (status)
    return svn_error_wrap_apr(status, _("Can't open file '%s'"),
                              svn_dirent_local_style(fname, pool));
  else
    return SVN_NO_ERROR;
}


static APR_INLINE svn_error_t *
do_io_file_wrapper_cleanup(apr_file_t *file, apr_status_t status,
                           const char *msg, const char *msg_no_name,
                           apr_pool_t *pool)
{
  const char *name;
  svn_error_t *err;

  if (! status)
    return SVN_NO_ERROR;

  err = svn_io_file_name_get(&name, file, pool);
  if (err)
    name = NULL;
  svn_error_clear(err);

  /* ### Issue #3014: Return a specific error for broken pipes,
   * ### with a single element in the error chain. */
  if (SVN__APR_STATUS_IS_EPIPE(status))
    return svn_error_create(SVN_ERR_IO_PIPE_WRITE_ERROR, NULL, NULL);

  if (name)
    return svn_error_wrap_apr(status, _(msg),
                              try_utf8_from_internal_style(name, pool));
  else
    return svn_error_wrap_apr(status, "%s", _(msg_no_name));
}


svn_error_t *
svn_io_file_close(apr_file_t *file, apr_pool_t *pool)
{
  return do_io_file_wrapper_cleanup(file, apr_file_close(file),
                                    N_("Can't close file '%s'"),
                                    N_("Can't close stream"),
                                    pool);
}


svn_error_t *
svn_io_file_getc(char *ch, apr_file_t *file, apr_pool_t *pool)
{
  return do_io_file_wrapper_cleanup(file, apr_file_getc(ch, file),
                                    N_("Can't read file '%s'"),
                                    N_("Can't read stream"),
                                    pool);
}


svn_error_t *
svn_io_file_putc(char ch, apr_file_t *file, apr_pool_t *pool)
{
  return do_io_file_wrapper_cleanup(file, apr_file_putc(ch, file),
                                    N_("Can't write file '%s'"),
                                    N_("Can't write stream"),
                                    pool);
}


svn_error_t *
svn_io_file_info_get(apr_finfo_t *finfo, apr_int32_t wanted,
                     apr_file_t *file, apr_pool_t *pool)
{
  /* Quoting APR: On NT this request is incredibly expensive, but accurate. */
  wanted &= ~SVN__APR_FINFO_MASK_OUT;

  return do_io_file_wrapper_cleanup(
             file, apr_file_info_get(finfo, wanted, file),
             N_("Can't get attribute information from file '%s'"),
             N_("Can't get attribute information from stream"),
             pool);
}


svn_error_t *
svn_io_file_read(apr_file_t *file, void *buf,
                 apr_size_t *nbytes, apr_pool_t *pool)
{
  return do_io_file_wrapper_cleanup(file, apr_file_read(file, buf, nbytes),
                                    N_("Can't read file '%s'"),
                                    N_("Can't read stream"),
                                    pool);
}


svn_error_t *
svn_io_file_read_full2(apr_file_t *file, void *buf,
                       apr_size_t nbytes, apr_size_t *bytes_read,
                       svn_boolean_t *hit_eof,
                       apr_pool_t *pool)
{
  apr_status_t status = apr_file_read_full(file, buf, nbytes, bytes_read);
  if (hit_eof)
    {
      if (APR_STATUS_IS_EOF(status))
        {
          *hit_eof = TRUE;
          return SVN_NO_ERROR;
        }
      else
        *hit_eof = FALSE;
    }

  return do_io_file_wrapper_cleanup(file, status,
                                    N_("Can't read file '%s'"),
                                    N_("Can't read stream"),
                                    pool);
}


svn_error_t *
svn_io_file_seek(apr_file_t *file, apr_seek_where_t where,
                 apr_off_t *offset, apr_pool_t *pool)
{
  return do_io_file_wrapper_cleanup(
             file, apr_file_seek(file, where, offset),
             N_("Can't set position pointer in file '%s'"),
             N_("Can't set position pointer in stream"),
             pool);
}

svn_error_t *
svn_io_file_aligned_seek(apr_file_t *file,
                         apr_off_t block_size,
                         apr_off_t *buffer_start,
                         apr_off_t offset,
                         apr_pool_t *scratch_pool)
{
  const apr_size_t apr_default_buffer_size = 4096;
  apr_size_t file_buffer_size = apr_default_buffer_size;
  apr_off_t desired_offset = 0;
  apr_off_t current = 0;
  apr_off_t aligned_offset = 0;
  svn_boolean_t fill_buffer = FALSE;

  /* paranoia check: huge blocks on 32 bit machines may cause overflows */
  SVN_ERR_ASSERT(block_size == (apr_size_t)block_size);

  /* default for invalid block sizes */
  if (block_size == 0)
    block_size = apr_default_buffer_size;

  file_buffer_size = apr_file_buffer_size_get(file);

  /* don't try to set a buffer size for non-buffered files! */
  if (file_buffer_size == 0)
    {
      aligned_offset = offset;
    }
  else if (file_buffer_size != (apr_size_t)block_size)
    {
      /* FILE has the wrong buffer size. correct it */
      char *buffer;
      file_buffer_size = (apr_size_t)block_size;
      buffer = apr_palloc(apr_file_pool_get(file), file_buffer_size);
      apr_file_buffer_set(file, buffer, file_buffer_size);

      /* seek to the start of the block and cause APR to read 1 block */
      aligned_offset = offset - (offset % block_size);
      fill_buffer = TRUE;
    }
  else
    {
      aligned_offset = offset - (offset % file_buffer_size);

      /* We have no way to determine the block start of an APR file.
         Furthermore, we don't want to throw away the current buffer
         contents.  Thus, we re-align the buffer only if the CURRENT
         offset definitely lies outside the desired, aligned buffer.
         This covers the typical case of linear reads getting very
         close to OFFSET but reading the previous / following block.

         Note that ALIGNED_OFFSET may still be within the current
         buffer and no I/O will actually happen in the FILL_BUFFER
         section below.
       */
      SVN_ERR(svn_io_file_seek(file, APR_CUR, &current, scratch_pool));
      fill_buffer = aligned_offset + file_buffer_size <= current
                 || current <= aligned_offset;
    }

  if (fill_buffer)
    {
      char dummy;
      apr_status_t status;

      /* seek to the start of the block and cause APR to read 1 block */
      SVN_ERR(svn_io_file_seek(file, APR_SET, &aligned_offset,
                               scratch_pool));
      status = apr_file_getc(&dummy, file);

      /* read may fail if we seek to or behind EOF.  That's ok then. */
      if (status != APR_SUCCESS && !APR_STATUS_IS_EOF(status))
        return do_io_file_wrapper_cleanup(file, status,
                                          N_("Can't read file '%s'"),
                                          N_("Can't read stream"),
                                          scratch_pool);
    }

  /* finally, seek to the OFFSET the caller wants */
  desired_offset = offset;
  SVN_ERR(svn_io_file_seek(file, APR_SET, &offset, scratch_pool));
  if (desired_offset != offset)
    return do_io_file_wrapper_cleanup(file, APR_EOF,
                                      N_("Can't seek in file '%s'"),
                                      N_("Can't seek in stream"),
                                      scratch_pool);

  /* return the buffer start that we (probably) enforced */
  if (buffer_start)
    *buffer_start = aligned_offset;

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_file_write(apr_file_t *file, const void *buf,
                  apr_size_t *nbytes, apr_pool_t *pool)
{
  return svn_error_trace(do_io_file_wrapper_cleanup(
     file, apr_file_write(file, buf, nbytes),
     N_("Can't write to file '%s'"),
     N_("Can't write to stream"),
     pool));
}

svn_error_t *
svn_io_file_flush(apr_file_t *file,
                  apr_pool_t *scratch_pool)
{
  return svn_error_trace(do_io_file_wrapper_cleanup(
     file, apr_file_flush(file),
     N_("Can't flush file '%s'"),
     N_("Can't flush stream"),
     scratch_pool));
}

svn_error_t *
svn_io_file_write_full(apr_file_t *file, const void *buf,
                       apr_size_t nbytes, apr_size_t *bytes_written,
                       apr_pool_t *pool)
{
  /* We cannot simply call apr_file_write_full on Win32 as it may fail
     for larger values of NBYTES. In that case, we have to emulate the
     "_full" part here. Thus, always call apr_file_write directly on
     Win32 as this minimizes overhead for small data buffers. */
#ifdef WIN32
#define MAXBUFSIZE 30*1024
  apr_size_t bw = nbytes;
  apr_size_t to_write = nbytes;

  /* try a simple "write everything at once" first */
  apr_status_t rv = apr_file_write(file, buf, &bw);
  buf = (char *)buf + bw;
  to_write -= bw;

  /* if the OS cannot handle that, use smaller chunks */
  if (rv == APR_FROM_OS_ERROR(ERROR_NOT_ENOUGH_MEMORY)
      && nbytes > MAXBUFSIZE)
    {
      do {
        bw = to_write > MAXBUFSIZE ? MAXBUFSIZE : to_write;
        rv = apr_file_write(file, buf, &bw);
        buf = (char *)buf + bw;
        to_write -= bw;
      } while (rv == APR_SUCCESS && to_write > 0);
    }

  /* bytes_written may actually be NULL */
  if (bytes_written)
    *bytes_written = nbytes - to_write;
#undef MAXBUFSIZE
#else
  apr_status_t rv = apr_file_write_full(file, buf, nbytes, bytes_written);
#endif

  return svn_error_trace(do_io_file_wrapper_cleanup(
     file, rv,
     N_("Can't write to file '%s'"),
     N_("Can't write to stream"),
     pool));
}


svn_error_t *
svn_io_write_unique(const char **tmp_path,
                    const char *dirpath,
                    const void *buf,
                    apr_size_t nbytes,
                    svn_io_file_del_t delete_when,
                    apr_pool_t *pool)
{
  apr_file_t *new_file;
  svn_error_t *err;

  SVN_ERR(svn_io_open_unique_file3(&new_file, tmp_path, dirpath,
                                   delete_when, pool, pool));

  err = svn_io_file_write_full(new_file, buf, nbytes, NULL, pool);

  if (!err)
    {
      /* svn_io_file_flush_to_disk() can be very expensive, so use the
         cheaper standard flush if the file is created as temporary file
         anyway */
      if (delete_when == svn_io_file_del_none)
        err = svn_io_file_flush_to_disk(new_file, pool);
      else
        err = svn_io_file_flush(new_file, pool);
    }

  return svn_error_trace(
                  svn_error_compose_create(err,
                                           svn_io_file_close(new_file, pool)));
}

svn_error_t *
svn_io_write_atomic(const char *final_path,
                    const void *buf,
                    apr_size_t nbytes,
                    const char *copy_perms_path,
                    apr_pool_t *scratch_pool)
{
  apr_file_t *tmp_file;
  const char *tmp_path;
  svn_error_t *err;
  const char *dirname = svn_dirent_dirname(final_path, scratch_pool);

  SVN_ERR(svn_io_open_unique_file3(&tmp_file, &tmp_path, dirname,
                                   svn_io_file_del_none,
                                   scratch_pool, scratch_pool));

  err = svn_io_file_write_full(tmp_file, buf, nbytes, NULL, scratch_pool);

  if (!err)
    err = svn_io_file_flush_to_disk(tmp_file, scratch_pool);

  err = svn_error_compose_create(err,
                                 svn_io_file_close(tmp_file, scratch_pool));

  if (!err && copy_perms_path)
    err = svn_io_copy_perms(copy_perms_path, tmp_path, scratch_pool);

  if (!err)
    err = svn_io_file_rename(tmp_path, final_path, scratch_pool);

  if (err)
    {
      err = svn_error_compose_create(err,
                                     svn_io_remove_file2(tmp_path, TRUE,
                                                         scratch_pool));

      return svn_error_createf(err->apr_err, err,
                               _("Can't write '%s' atomically"),
                               svn_dirent_local_style(final_path,
                                                      scratch_pool));
    }

#ifdef __linux__
  {
    /* Linux has the unusual feature that fsync() on a file is not
       enough to ensure that a file's directory entries have been
       flushed to disk; you have to fsync the directory as well.
       On other operating systems, we'd only be asking for trouble
       by trying to open and fsync a directory. */
    apr_file_t *file;

    SVN_ERR(svn_io_file_open(&file, dirname, APR_READ, APR_OS_DEFAULT,
                             scratch_pool));
    SVN_ERR(svn_io_file_flush_to_disk(file, scratch_pool));
    SVN_ERR(svn_io_file_close(file, scratch_pool));
  }
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_file_trunc(apr_file_t *file, apr_off_t offset, apr_pool_t *pool)
{
  /* This is a work-around. APR would flush the write buffer
     _after_ truncating the file causing now invalid buffered
     data to be written behind OFFSET. */
  SVN_ERR(do_io_file_wrapper_cleanup(file, apr_file_flush(file),
                                     N_("Can't flush file '%s'"),
                                     N_("Can't flush stream"),
                                     pool));

  return do_io_file_wrapper_cleanup(file, apr_file_trunc(file, offset),
                                    N_("Can't truncate file '%s'"),
                                    N_("Can't truncate stream"),
                                    pool);
}


svn_error_t *
svn_io_read_length_line(apr_file_t *file, char *buf, apr_size_t *limit,
                        apr_pool_t *pool)
{
  /* variables */
  apr_size_t total_read = 0;
  svn_boolean_t eof = FALSE;
  const char *name;
  svn_error_t *err;
  apr_size_t buf_size = *limit;

  while (buf_size > 0)
    {
      /* read a fair chunk of data at once. But don't get too ambitious
       * as that would result in too much waste. Also make sure we can
       * put a NUL after the last byte read.
       */
      apr_size_t to_read = buf_size < 129 ? buf_size - 1 : 128;
      apr_size_t bytes_read = 0;
      char *eol;

      if (to_read == 0)
        break;

      /* read data block (or just a part of it) */
      SVN_ERR(svn_io_file_read_full2(file, buf, to_read,
                                     &bytes_read, &eof, pool));

      /* look or a newline char */
      buf[bytes_read] = 0;
      eol = strchr(buf, '\n');
      if (eol)
        {
          apr_off_t offset = (eol + 1 - buf) - (apr_off_t)bytes_read;

          *eol = 0;
          *limit = total_read + (eol - buf);

          /* correct the file pointer:
           * appear as though we just had read the newline char
           */
          SVN_ERR(svn_io_file_seek(file, APR_CUR, &offset, pool));

          return SVN_NO_ERROR;
        }
      else if (eof)
        {
          /* no EOL found but we hit the end of the file.
           * Generate a nice EOF error object and return it.
           */
          char dummy;
          SVN_ERR(svn_io_file_getc(&dummy, file, pool));
        }

      /* next data chunk */
      buf_size -= bytes_read;
      buf += bytes_read;
      total_read += bytes_read;
    }

  /* buffer limit has been exceeded without finding the EOL */
  err = svn_io_file_name_get(&name, file, pool);
  if (err)
    name = NULL;
  svn_error_clear(err);

  if (name)
    return svn_error_createf(SVN_ERR_MALFORMED_FILE, NULL,
                             _("Can't read length line in file '%s'"),
                             svn_dirent_local_style(name, pool));
  else
    return svn_error_create(SVN_ERR_MALFORMED_FILE, NULL,
                            _("Can't read length line in stream"));
}


svn_error_t *
svn_io_stat(apr_finfo_t *finfo, const char *fname,
            apr_int32_t wanted, apr_pool_t *pool)
{
  apr_status_t status;
  const char *fname_apr;

  /* APR doesn't like "" directories */
  if (fname[0] == '\0')
    fname = ".";

  SVN_ERR(cstring_from_utf8(&fname_apr, fname, pool));

  /* Quoting APR: On NT this request is incredibly expensive, but accurate. */
  wanted &= ~SVN__APR_FINFO_MASK_OUT;

  status = apr_stat(finfo, fname_apr, wanted, pool);
  if (status)
    return svn_error_wrap_apr(status, _("Can't stat '%s'"),
                              svn_dirent_local_style(fname, pool));

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_file_rename(const char *from_path, const char *to_path,
                   apr_pool_t *pool)
{
  apr_status_t status = APR_SUCCESS;
  const char *from_path_apr, *to_path_apr;

  SVN_ERR(cstring_from_utf8(&from_path_apr, from_path, pool));
  SVN_ERR(cstring_from_utf8(&to_path_apr, to_path, pool));

  status = apr_file_rename(from_path_apr, to_path_apr, pool);

#if defined(WIN32) || defined(__OS2__) || defined(__CYGWIN__)
  /* If the target file is read only NTFS reports EACCESS and
     FAT/FAT32 reports EEXIST */
  if (APR_STATUS_IS_EACCES(status) || APR_STATUS_IS_EEXIST(status))
    {
#ifndef __CYGWIN__
      /* Set the destination file writable because Windows will not
         allow us to rename when to_path is read-only, but will
         allow renaming when from_path is read only. */
      SVN_ERR(svn_io_set_file_read_write(to_path, TRUE, pool));
#endif

      status = apr_file_rename(from_path_apr, to_path_apr, pool);
    }
  WIN32_RETRY_LOOP(status, apr_file_rename(from_path_apr, to_path_apr, pool));
#endif /* WIN32 || __OS2__ || __CYGWIN__ */

  if (status)
    return svn_error_wrap_apr(status, _("Can't move '%s' to '%s'"),
                              svn_dirent_local_style(from_path, pool),
                              svn_dirent_local_style(to_path, pool));

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_file_move(const char *from_path, const char *to_path,
                 apr_pool_t *pool)
{
  svn_error_t *err = svn_io_file_rename(from_path, to_path, pool);

  if (err && APR_STATUS_IS_EXDEV(err->apr_err))
    {
      const char *tmp_to_path;

      svn_error_clear(err);

      SVN_ERR(svn_io_open_unique_file3(NULL, &tmp_to_path,
                                       svn_dirent_dirname(to_path, pool),
                                       svn_io_file_del_none,
                                       pool, pool));

      err = svn_io_copy_file(from_path, tmp_to_path, TRUE, pool);
      if (err)
        goto failed_tmp;

      err = svn_io_file_rename(tmp_to_path, to_path, pool);
      if (err)
        goto failed_tmp;

      err = svn_io_remove_file2(from_path, FALSE, pool);
      if (! err)
        return SVN_NO_ERROR;

      svn_error_clear(svn_io_remove_file2(to_path, FALSE, pool));

      return err;

    failed_tmp:
      svn_error_clear(svn_io_remove_file2(tmp_to_path, FALSE, pool));
    }

  return err;
}

/* Common implementation of svn_io_dir_make and svn_io_dir_make_hidden.
   HIDDEN determines if the hidden attribute
   should be set on the newly created directory. */
static svn_error_t *
dir_make(const char *path, apr_fileperms_t perm,
         svn_boolean_t hidden, svn_boolean_t sgid, apr_pool_t *pool)
{
  apr_status_t status;
  const char *path_apr;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  /* APR doesn't like "" directories */
  if (path_apr[0] == '\0')
    path_apr = ".";

#if (APR_OS_DEFAULT & APR_WSTICKY)
  /* The APR shipped with httpd 2.0.50 contains a bug where
     APR_OS_DEFAULT encompasses the setuid, setgid, and sticky bits.
     There is a special case for file creation, but not directory
     creation, so directories wind up getting created with the sticky
     bit set.  (There is no such thing as a setuid directory, and the
     setgid bit is apparently ignored at mkdir() time.)  If we detect
     this problem, work around it by unsetting those bits if we are
     passed APR_OS_DEFAULT. */
  if (perm == APR_OS_DEFAULT)
    perm &= ~(APR_USETID | APR_GSETID | APR_WSTICKY);
#endif

  status = apr_dir_make(path_apr, perm, pool);

#ifdef WIN32
  /* Don't retry on ERROR_ACCESS_DENIED, as that typically signals a
     permanent error */
  if (status == APR_FROM_OS_ERROR(ERROR_SHARING_VIOLATION))
    WIN32_RETRY_LOOP(status, apr_dir_make(path_apr, perm, pool));
#endif

  if (status)
    return svn_error_wrap_apr(status, _("Can't create directory '%s'"),
                              svn_dirent_local_style(path, pool));

#ifdef APR_FILE_ATTR_HIDDEN
  if (hidden)
    {
#ifndef WIN32
      status = apr_file_attrs_set(path_apr,
                                  APR_FILE_ATTR_HIDDEN,
                                  APR_FILE_ATTR_HIDDEN,
                                  pool);
      if (status)
        return svn_error_wrap_apr(status, _("Can't hide directory '%s'"),
                                  svn_dirent_local_style(path, pool));
#else
    /* on Windows, use our wrapper so we can also set the
       FILE_ATTRIBUTE_NOT_CONTENT_INDEXED attribute */
      svn_error_t *err =
          io_win_file_attrs_set(path_apr,
                                FILE_ATTRIBUTE_HIDDEN |
                                FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
                                FILE_ATTRIBUTE_HIDDEN |
                                FILE_ATTRIBUTE_NOT_CONTENT_INDEXED,
                                pool);
      if (err)
        return svn_error_createf(err->apr_err, err,
                                 _("Can't hide directory '%s'"),
                                 svn_dirent_local_style(path, pool));
#endif /* WIN32 */
    }
#endif /* APR_FILE_ATTR_HIDDEN */

/* Windows does not implement sgid. Skip here because retrieving
   the file permissions via APR_FINFO_PROT | APR_FINFO_OWNER is documented
   to be 'incredibly expensive'. */
#if !defined(WIN32) && !defined(__CYGWIN__)
  if (sgid)
    {
      apr_finfo_t finfo;

      /* Per our contract, don't do error-checking.  Some filesystems
       * don't support the sgid bit, and that's okay. */
      status = apr_stat(&finfo, path_apr, APR_FINFO_PROT, pool);

      if (!status)
        apr_file_perms_set(path_apr, finfo.protection | APR_GSETID);
    }
#endif

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_dir_make(const char *path, apr_fileperms_t perm, apr_pool_t *pool)
{
  return dir_make(path, perm, FALSE, FALSE, pool);
}

svn_error_t *
svn_io_dir_make_hidden(const char *path, apr_fileperms_t perm,
                       apr_pool_t *pool)
{
  return dir_make(path, perm, TRUE, FALSE, pool);
}

svn_error_t *
svn_io_dir_make_sgid(const char *path, apr_fileperms_t perm,
                     apr_pool_t *pool)
{
  return dir_make(path, perm, FALSE, TRUE, pool);
}


svn_error_t *
svn_io_dir_open(apr_dir_t **new_dir, const char *dirname, apr_pool_t *pool)
{
  apr_status_t status;
  const char *dirname_apr;

  /* APR doesn't like "" directories */
  if (dirname[0] == '\0')
    dirname = ".";

  SVN_ERR(cstring_from_utf8(&dirname_apr, dirname, pool));

  status = apr_dir_open(new_dir, dirname_apr, pool);
  if (status)
    return svn_error_wrap_apr(status, _("Can't open directory '%s'"),
                              svn_dirent_local_style(dirname, pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_dir_remove_nonrecursive(const char *dirname, apr_pool_t *pool)
{
  apr_status_t status;
  const char *dirname_apr;

  SVN_ERR(cstring_from_utf8(&dirname_apr, dirname, pool));

  status = apr_dir_remove(dirname_apr, pool);

#ifdef WIN32
  {
    svn_boolean_t retry = TRUE;

    if (APR_TO_OS_ERROR(status) == ERROR_DIR_NOT_EMPTY)
      {
        apr_status_t empty_status = dir_is_empty(dirname_apr, pool);

        if (APR_STATUS_IS_ENOTEMPTY(empty_status))
          retry = FALSE;
      }

    if (retry)
      {
        WIN32_RETRY_LOOP(status, apr_dir_remove(dirname_apr, pool));
      }
  }
#endif
  if (status)
    return svn_error_wrap_apr(status, _("Can't remove directory '%s'"),
                              svn_dirent_local_style(dirname, pool));

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_dir_read(apr_finfo_t *finfo,
                apr_int32_t wanted,
                apr_dir_t *thedir,
                apr_pool_t *pool)
{
  apr_status_t status;

  status = apr_dir_read(finfo, wanted, thedir);

  if (status)
    return svn_error_wrap_apr(status, _("Can't read directory"));

  /* It would be nice to use entry_name_to_utf8() below, but can we
     get the dir's path out of an apr_dir_t?  I don't see a reliable
     way to do it. */

  if (finfo->fname)
    SVN_ERR(svn_path_cstring_to_utf8(&finfo->fname, finfo->fname, pool));

  if (finfo->name)
    SVN_ERR(svn_path_cstring_to_utf8(&finfo->name, finfo->name, pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_dir_close(apr_dir_t *thedir)
{
  apr_status_t apr_err = apr_dir_close(thedir);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Error closing directory"));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_dir_walk2(const char *dirname,
                 apr_int32_t wanted,
                 svn_io_walk_func_t walk_func,
                 void *walk_baton,
                 apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_dir_t *handle;
  apr_pool_t *subpool;
  const char *dirname_apr;
  apr_finfo_t finfo;

  wanted |= APR_FINFO_TYPE | APR_FINFO_NAME;

  /* Quoting APR: On NT this request is incredibly expensive, but accurate. */
  wanted &= ~SVN__APR_FINFO_MASK_OUT;

  /* The documentation for apr_dir_read used to state that "." and ".."
     will be returned as the first two files, but it doesn't
     work that way in practice, in particular ext3 on Linux-2.6 doesn't
     follow the rules.  For details see
     http://subversion.tigris.org/servlets/ReadMsg?list=dev&msgNo=56666

     If APR ever does implement "dot-first" then it would be possible to
     remove the svn_io_stat and walk_func calls and use the walk_func
     inside the loop.

     Note: apr_stat doesn't handle FINFO_NAME but svn_io_dir_walk is
     documented to provide it, so we have to do a bit extra. */
  SVN_ERR(svn_io_stat(&finfo, dirname, wanted & ~APR_FINFO_NAME, pool));
  SVN_ERR(cstring_from_utf8(&finfo.name,
                            svn_dirent_basename(dirname, pool),
                            pool));
  finfo.valid |= APR_FINFO_NAME;
  SVN_ERR((*walk_func)(walk_baton, dirname, &finfo, pool));

  SVN_ERR(cstring_from_utf8(&dirname_apr, dirname, pool));

  /* APR doesn't like "" directories */
  if (dirname_apr[0] == '\0')
    dirname_apr = ".";

  apr_err = apr_dir_open(&handle, dirname_apr, pool);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Can't open directory '%s'"),
                              svn_dirent_local_style(dirname, pool));

  /* iteration subpool */
  subpool = svn_pool_create(pool);

  while (1)
    {
      const char *name_utf8;
      const char *full_path;

      svn_pool_clear(subpool);

      apr_err = apr_dir_read(&finfo, wanted, handle);
      if (APR_STATUS_IS_ENOENT(apr_err))
        break;
      else if (apr_err)
        {
          return svn_error_wrap_apr(apr_err,
                                    _("Can't read directory entry in '%s'"),
                                    svn_dirent_local_style(dirname, pool));
        }

      if (finfo.filetype == APR_DIR)
        {
          if (finfo.name[0] == '.'
              && (finfo.name[1] == '\0'
                  || (finfo.name[1] == '.' && finfo.name[2] == '\0')))
            /* skip "." and ".." */
            continue;

          /* some other directory. recurse. it will be passed to the
             callback inside the recursion. */
          SVN_ERR(entry_name_to_utf8(&name_utf8, finfo.name, dirname,
                                     subpool));
          full_path = svn_dirent_join(dirname, name_utf8, subpool);
          SVN_ERR(svn_io_dir_walk2(full_path,
                                   wanted,
                                   walk_func,
                                   walk_baton,
                                   subpool));
        }
      else if (finfo.filetype == APR_REG || finfo.filetype == APR_LNK)
        {
          /* some other directory. pass it to the callback. */
          SVN_ERR(entry_name_to_utf8(&name_utf8, finfo.name, dirname,
                                     subpool));
          full_path = svn_dirent_join(dirname, name_utf8, subpool);
          SVN_ERR((*walk_func)(walk_baton,
                               full_path,
                               &finfo,
                               subpool));
        }
      /* else:
         Some other type of file; skip it for now.  We've reserved the
         right to expand our coverage here in the future, though,
         without revving this API.
      */
    }

  svn_pool_destroy(subpool);

  apr_err = apr_dir_close(handle);
  if (apr_err)
    return svn_error_wrap_apr(apr_err, _("Error closing directory '%s'"),
                              svn_dirent_local_style(dirname, pool));

  return SVN_NO_ERROR;
}



/**
 * Determine if a directory is empty or not.
 * @param Return APR_SUCCESS if the dir is empty, else APR_ENOTEMPTY if not.
 * @param path The directory.
 * @param pool Used for temporary allocation.
 * @remark If path is not a directory, or some other error occurs,
 * then return the appropriate apr status code.
 *
 * (This function is written in APR style, in anticipation of
 * perhaps someday being moved to APR as 'apr_dir_is_empty'.)
 */
static apr_status_t
dir_is_empty(const char *dir, apr_pool_t *pool)
{
  apr_status_t apr_err;
  apr_dir_t *dir_handle;
  apr_finfo_t finfo;
  apr_status_t retval = APR_SUCCESS;

  /* APR doesn't like "" directories */
  if (dir[0] == '\0')
    dir = ".";

  apr_err = apr_dir_open(&dir_handle, dir, pool);
  if (apr_err != APR_SUCCESS)
    return apr_err;

  for (apr_err = apr_dir_read(&finfo, APR_FINFO_NAME, dir_handle);
       apr_err == APR_SUCCESS;
       apr_err = apr_dir_read(&finfo, APR_FINFO_NAME, dir_handle))
    {
      /* Ignore entries for this dir and its parent, robustly.
         (APR promises that they'll come first, so technically
         this guard could be moved outside the loop.  But Ryan Bloom
         says he doesn't believe it, and I believe him. */
      if (! (finfo.name[0] == '.'
             && (finfo.name[1] == '\0'
                 || (finfo.name[1] == '.' && finfo.name[2] == '\0'))))
        {
          retval = APR_ENOTEMPTY;
          break;
        }
    }

  /* Make sure we broke out of the loop for the right reason. */
  if (apr_err && ! APR_STATUS_IS_ENOENT(apr_err))
    return apr_err;

  apr_err = apr_dir_close(dir_handle);
  if (apr_err != APR_SUCCESS)
    return apr_err;

  return retval;
}


svn_error_t *
svn_io_dir_empty(svn_boolean_t *is_empty_p,
                 const char *path,
                 apr_pool_t *pool)
{
  apr_status_t status;
  const char *path_apr;

  SVN_ERR(cstring_from_utf8(&path_apr, path, pool));

  status = dir_is_empty(path_apr, pool);

  if (!status)
    *is_empty_p = TRUE;
  else if (APR_STATUS_IS_ENOTEMPTY(status))
    *is_empty_p = FALSE;
  else
    return svn_error_wrap_apr(status, _("Can't check directory '%s'"),
                              svn_dirent_local_style(path, pool));

  return SVN_NO_ERROR;
}



/*** Version/format files ***/

svn_error_t *
svn_io_write_version_file(const char *path,
                          int version,
                          apr_pool_t *pool)
{
  const char *path_tmp;
  const char *format_contents = apr_psprintf(pool, "%d\n", version);

  SVN_ERR_ASSERT(version >= 0);

  SVN_ERR(svn_io_write_unique(&path_tmp,
                              svn_dirent_dirname(path, pool),
                              format_contents, strlen(format_contents),
                              svn_io_file_del_none, pool));

#if defined(WIN32) || defined(__OS2__)
  /* make the destination writable, but only on Windows, because
     Windows does not let us replace read-only files. */
  SVN_ERR(svn_io_set_file_read_write(path, TRUE, pool));
#endif /* WIN32 || __OS2__ */

  /* rename the temp file as the real destination */
  SVN_ERR(svn_io_file_rename(path_tmp, path, pool));

  /* And finally remove the perms to make it read only */
  return svn_io_set_file_read_only(path, FALSE, pool);
}


svn_error_t *
svn_io_read_version_file(int *version,
                         const char *path,
                         apr_pool_t *pool)
{
  apr_file_t *format_file;
  char buf[80];
  apr_size_t len;
  svn_error_t *err;

  /* Read a chunk of data from PATH */
  SVN_ERR(svn_io_file_open(&format_file, path, APR_READ,
                           APR_OS_DEFAULT, pool));
  len = sizeof(buf);
  err = svn_io_file_read(format_file, buf, &len, pool);

  /* Close the file. */
  SVN_ERR(svn_error_compose_create(err,
                                   svn_io_file_close(format_file, pool)));

  /* If there was no data in PATH, return an error. */
  if (len == 0)
    return svn_error_createf(SVN_ERR_STREAM_UNEXPECTED_EOF, NULL,
                             _("Reading '%s'"),
                             svn_dirent_local_style(path, pool));

  /* Check that the first line contains only digits. */
  {
    apr_size_t i;

    for (i = 0; i < len; ++i)
      {
        char c = buf[i];

        if (i > 0 && (c == '\r' || c == '\n'))
          {
            buf[i] = '\0';
            break;
          }
        if (! svn_ctype_isdigit(c))
          return svn_error_createf
            (SVN_ERR_BAD_VERSION_FILE_FORMAT, NULL,
             _("First line of '%s' contains non-digit"),
             svn_dirent_local_style(path, pool));
      }
  }

  /* Convert to integer. */
  SVN_ERR(svn_cstring_atoi(version, buf));

  return SVN_NO_ERROR;
}


/* Do a byte-for-byte comparison of FILE1 and FILE2. */
static svn_error_t *
contents_identical_p(svn_boolean_t *identical_p,
                     const char *file1,
                     const char *file2,
                     apr_pool_t *pool)
{
  svn_error_t *err;
  apr_size_t bytes_read1, bytes_read2;
  char *buf1 = apr_palloc(pool, SVN__STREAM_CHUNK_SIZE);
  char *buf2 = apr_palloc(pool, SVN__STREAM_CHUNK_SIZE);
  apr_file_t *file1_h;
  apr_file_t *file2_h;
  svn_boolean_t eof1 = FALSE;
  svn_boolean_t eof2 = FALSE;

  SVN_ERR(svn_io_file_open(&file1_h, file1, APR_READ, APR_OS_DEFAULT,
                           pool));

  err = svn_io_file_open(&file2_h, file2, APR_READ, APR_OS_DEFAULT,
                         pool);

  if (err)
    return svn_error_trace(
               svn_error_compose_create(err,
                                        svn_io_file_close(file1_h, pool)));

  *identical_p = TRUE;  /* assume TRUE, until disproved below */
  while (!err && !eof1 && !eof2)
    {
      err = svn_io_file_read_full2(file1_h, buf1,
                                   SVN__STREAM_CHUNK_SIZE, &bytes_read1,
                                   &eof1, pool);
      if (err)
          break;

      err = svn_io_file_read_full2(file2_h, buf2,
                                   SVN__STREAM_CHUNK_SIZE, &bytes_read2,
                                   &eof2, pool);
      if (err)
          break;

      if ((bytes_read1 != bytes_read2) || memcmp(buf1, buf2, bytes_read1))
        {
          *identical_p = FALSE;
          break;
        }
    }

  /* Special case: one file being a prefix of the other and the shorter
   * file's size is a multiple of SVN__STREAM_CHUNK_SIZE. */
  if (!err && (eof1 != eof2))
    *identical_p = FALSE;

  return svn_error_trace(
           svn_error_compose_create(
                err,
                svn_error_compose_create(svn_io_file_close(file1_h, pool),
                                         svn_io_file_close(file2_h, pool))));
}



/* Do a byte-for-byte comparison of FILE1, FILE2 and FILE3. */
static svn_error_t *
contents_three_identical_p(svn_boolean_t *identical_p12,
                           svn_boolean_t *identical_p23,
                           svn_boolean_t *identical_p13,
                           const char *file1,
                           const char *file2,
                           const char *file3,
                           apr_pool_t *scratch_pool)
{
  svn_error_t *err;
  char *buf1 = apr_palloc(scratch_pool, SVN__STREAM_CHUNK_SIZE);
  char *buf2 = apr_palloc(scratch_pool, SVN__STREAM_CHUNK_SIZE);
  char *buf3 = apr_palloc(scratch_pool, SVN__STREAM_CHUNK_SIZE);
  apr_file_t *file1_h;
  apr_file_t *file2_h;
  apr_file_t *file3_h;
  svn_boolean_t eof1 = FALSE;
  svn_boolean_t eof2 = FALSE;
  svn_boolean_t eof3 = FALSE;

  SVN_ERR(svn_io_file_open(&file1_h, file1, APR_READ, APR_OS_DEFAULT,
                           scratch_pool));

  err = svn_io_file_open(&file2_h, file2, APR_READ, APR_OS_DEFAULT,
                         scratch_pool);

  if (err)
    return svn_error_trace(
               svn_error_compose_create(err,
                                        svn_io_file_close(file1_h, scratch_pool)));

  err = svn_io_file_open(&file3_h, file3, APR_READ, APR_OS_DEFAULT,
                         scratch_pool);

  if (err)
      return svn_error_trace(
               svn_error_compose_create(
                    err,
                    svn_error_compose_create(svn_io_file_close(file1_h,
                                                          scratch_pool),
                                             svn_io_file_close(file2_h,
                                                          scratch_pool))));

  /* assume TRUE, until disproved below */
  *identical_p12 = *identical_p23 = *identical_p13 = TRUE;
  /* We need to read as long as no error occurs, and as long as one of the
   * flags could still change due to a read operation */
  while (!err
        && ((*identical_p12 && !eof1 && !eof2)
            || (*identical_p23 && !eof2 && !eof3)
            || (*identical_p13 && !eof1 && !eof3)))
    {
      apr_size_t bytes_read1, bytes_read2, bytes_read3;
      svn_boolean_t read_1, read_2, read_3;

      read_1 = read_2 = read_3 = FALSE;

      /* As long as a file is not at the end yet, and it is still
       * potentially identical to another file, we read the next chunk.*/
      if (!eof1 && (*identical_p12 || *identical_p13))
        {
          err = svn_io_file_read_full2(file1_h, buf1,
                                   SVN__STREAM_CHUNK_SIZE, &bytes_read1,
                                   &eof1, scratch_pool);
          if (err)
              break;
          read_1 = TRUE;
        }

      if (!eof2 && (*identical_p12 || *identical_p23))
        {
          err = svn_io_file_read_full2(file2_h, buf2,
                                   SVN__STREAM_CHUNK_SIZE, &bytes_read2,
                                   &eof2, scratch_pool);
          if (err)
              break;
          read_2 = TRUE;
        }

      if (!eof3 && (*identical_p13 || *identical_p23))
        {
          err = svn_io_file_read_full2(file3_h, buf3,
                                   SVN__STREAM_CHUNK_SIZE, &bytes_read3,
                                   &eof3, scratch_pool);
          if (err)
              break;
          read_3 = TRUE;
        }

      /* If the files are still marked identical, and at least one of them
       * is not at the end of file, we check whether they differ, and set
       * their flag to false then. */
      if (*identical_p12
          && (read_1 || read_2)
          && ((eof1 != eof2)
              || (bytes_read1 != bytes_read2)
              || memcmp(buf1, buf2, bytes_read1)))
        {
          *identical_p12 = FALSE;
        }

      if (*identical_p23
          && (read_2 || read_3)
          && ((eof2 != eof3)
              || (bytes_read2 != bytes_read3)
              || memcmp(buf2, buf3, bytes_read2)))
        {
          *identical_p23 = FALSE;
        }

      if (*identical_p13
          && (read_1 || read_3)
          && ((eof1 != eof3)
              || (bytes_read1 != bytes_read3)
              || memcmp(buf1, buf3, bytes_read3)))
        {
          *identical_p13 = FALSE;
        }
    }

  return svn_error_trace(
           svn_error_compose_create(
                err,
                svn_error_compose_create(
                    svn_io_file_close(file1_h, scratch_pool),
                    svn_error_compose_create(
                        svn_io_file_close(file2_h, scratch_pool),
                        svn_io_file_close(file3_h, scratch_pool)))));
}



svn_error_t *
svn_io_files_contents_same_p(svn_boolean_t *same,
                             const char *file1,
                             const char *file2,
                             apr_pool_t *pool)
{
  svn_boolean_t q;

  SVN_ERR(svn_io_filesizes_different_p(&q, file1, file2, pool));

  if (q)
    {
      *same = FALSE;
      return SVN_NO_ERROR;
    }

  SVN_ERR(contents_identical_p(&q, file1, file2, pool));

  if (q)
    *same = TRUE;
  else
    *same = FALSE;

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_files_contents_three_same_p(svn_boolean_t *same12,
                                   svn_boolean_t *same23,
                                   svn_boolean_t *same13,
                                   const char *file1,
                                   const char *file2,
                                   const char *file3,
                                   apr_pool_t *scratch_pool)
{
  svn_boolean_t diff_size12, diff_size23, diff_size13;

  SVN_ERR(svn_io_filesizes_three_different_p(&diff_size12,
                                             &diff_size23,
                                             &diff_size13,
                                             file1,
                                             file2,
                                             file3,
                                             scratch_pool));

  if (diff_size12 && diff_size23 && diff_size13)
    {
      *same12 = *same23 = *same13 = FALSE;
    }
  else if (diff_size12 && diff_size23)
    {
      *same12 = *same23 = FALSE;
      SVN_ERR(contents_identical_p(same13, file1, file3, scratch_pool));
    }
  else if (diff_size23 && diff_size13)
    {
      *same23 = *same13 = FALSE;
      SVN_ERR(contents_identical_p(same12, file1, file2, scratch_pool));
    }
  else if (diff_size12 && diff_size13)
    {
      *same12 = *same13 = FALSE;
      SVN_ERR(contents_identical_p(same23, file2, file3, scratch_pool));
    }
  else
    {
      SVN_ERR_ASSERT(!diff_size12 && !diff_size23 && !diff_size13);
      SVN_ERR(contents_three_identical_p(same12, same23, same13,
                                         file1, file2, file3,
                                         scratch_pool));
    }

  return SVN_NO_ERROR;
}

#ifdef WIN32
/* Counter value of file_mktemp request (used in a threadsafe way), to make
   sure that a single process normally never generates the same tempname
   twice */
static volatile apr_uint32_t tempname_counter = 0;
#endif

/* Creates a new temporary file in DIRECTORY with apr flags FLAGS.
   Set *NEW_FILE to the file handle and *NEW_FILE_NAME to its name.
   Perform temporary allocations in SCRATCH_POOL and the result in
   RESULT_POOL. */
static svn_error_t *
temp_file_create(apr_file_t **new_file,
                 const char **new_file_name,
                 const char *directory,
                 apr_int32_t flags,
                 apr_pool_t *result_pool,
                 apr_pool_t *scratch_pool)
{
#ifndef WIN32
  const char *templ = svn_dirent_join(directory, "svn-XXXXXX", scratch_pool);
  const char *templ_apr;
  apr_status_t status;

  SVN_ERR(svn_path_cstring_from_utf8(&templ_apr, templ, scratch_pool));

  /* ### svn_path_cstring_from_utf8() guarantees to make a copy of the
         data available in POOL and we need a non-const pointer here,
         as apr changes the template to return the new filename. */
  status = apr_file_mktemp(new_file, (char *)templ_apr, flags, result_pool);

  if (status)
    return svn_error_wrap_apr(status, _("Can't create temporary file from "
                              "template '%s'"), templ);

  /* Translate the returned path back to utf-8 before returning it */
  return svn_error_trace(svn_path_cstring_to_utf8(new_file_name,
                                                  templ_apr,
                                                  result_pool));
#else
  /* The Windows implementation of apr_file_mktemp doesn't handle access
     denied errors correctly. Therefore we implement our own temp file
     creation function here. */

  /* ### Most of this is borrowed from the svn_io_open_uniquely_named(),
     ### the function we used before. But we try to guess a more unique
     ### name before trying if it exists. */

  /* Offset by some time value and a unique request nr to make the number
     +- unique for both this process and on the computer */
  int baseNr = (GetTickCount() << 11) + 7 * svn_atomic_inc(&tempname_counter)
               + GetCurrentProcessId();
  int i;

  /* ### Maybe use an iterpool? */
  for (i = 0; i <= 99999; i++)
    {
      apr_uint32_t unique_nr;
      const char *unique_name;
      const char *unique_name_apr;
      apr_file_t *try_file;
      apr_status_t apr_err;

      /* Generate a number that should be unique for this application and
         usually for the entire computer to reduce the number of cycles
         through this loop. (A bit of calculation is much cheaper then
         disk io) */
      unique_nr = baseNr + 3 * i;

      unique_name = svn_dirent_join(directory,
                                    apr_psprintf(scratch_pool, "svn-%X",
                                                 unique_nr),
                                    scratch_pool);

      SVN_ERR(cstring_from_utf8(&unique_name_apr, unique_name, scratch_pool));

      apr_err = file_open(&try_file, unique_name_apr, flags,
                          APR_OS_DEFAULT, FALSE, scratch_pool);

      if (APR_STATUS_IS_EEXIST(apr_err))
          continue;
      else if (apr_err)
        {
          /* On Win32, CreateFile fails with an "Access Denied" error
             code, rather than "File Already Exists", if the colliding
             name belongs to a directory. */

          if (APR_STATUS_IS_EACCES(apr_err))
            {
              apr_finfo_t finfo;
              apr_status_t apr_err_2 = apr_stat(&finfo, unique_name_apr,
                                                APR_FINFO_TYPE, scratch_pool);

              if (!apr_err_2 && finfo.filetype == APR_DIR)
                continue;

              apr_err_2 = APR_TO_OS_ERROR(apr_err);

              if (apr_err_2 == ERROR_ACCESS_DENIED ||
                  apr_err_2 == ERROR_SHARING_VIOLATION)
                {
                  /* The file is in use by another process or is hidden;
                     create a new name, but don't do this 99999 times in
                     case the folder is not writable */
                  i += 797;
                  continue;
                }

              /* Else fall through and return the original error. */
            }

          return svn_error_wrap_apr(apr_err, _("Can't open '%s'"),
                                    svn_dirent_local_style(unique_name,
                                                           scratch_pool));
        }
      else
        {
          /* Move file to the right pool */
          apr_err = apr_file_setaside(new_file, try_file, result_pool);

          if (apr_err)
            return svn_error_wrap_apr(apr_err, _("Can't set aside '%s'"),
                                      svn_dirent_local_style(unique_name,
                                                             scratch_pool));

          *new_file_name = apr_pstrdup(result_pool, unique_name);

          return SVN_NO_ERROR;
        }
    }

  return svn_error_createf(SVN_ERR_IO_UNIQUE_NAMES_EXHAUSTED,
                           NULL,
                           _("Unable to make name in '%s'"),
                           svn_dirent_local_style(directory, scratch_pool));
#endif
}

/* Wrapper for apr_file_name_get(), passing out a UTF8-encoded filename. */
svn_error_t *
svn_io_file_name_get(const char **filename,
                     apr_file_t *file,
                     apr_pool_t *pool)
{
  const char *fname_apr;
  apr_status_t status;

  status = apr_file_name_get(&fname_apr, file);
  if (status)
    return svn_error_wrap_apr(status, _("Can't get file name"));

  if (fname_apr)
    SVN_ERR(svn_path_cstring_to_utf8(filename, fname_apr, pool));
  else
    *filename = NULL;

  return SVN_NO_ERROR;
}


svn_error_t *
svn_io_open_unique_file3(apr_file_t **file,
                         const char **unique_path,
                         const char *dirpath,
                         svn_io_file_del_t delete_when,
                         apr_pool_t *result_pool,
                         apr_pool_t *scratch_pool)
{
  apr_file_t *tempfile;
  const char *tempname;
  struct temp_file_cleanup_s *baton = NULL;
  apr_int32_t flags = (APR_READ | APR_WRITE | APR_CREATE | APR_EXCL |
                       APR_BUFFERED | APR_BINARY);
#if !defined(WIN32) && !defined(__OS2__)
  apr_fileperms_t perms;
  svn_boolean_t using_system_temp_dir = FALSE;
#endif

  SVN_ERR_ASSERT(file || unique_path);
  if (file)
    *file = NULL;
  if (unique_path)
    *unique_path = NULL;

  if (dirpath == NULL)
    {
#if !defined(WIN32) && !defined(__OS2__)
      using_system_temp_dir = TRUE;
#endif
      SVN_ERR(svn_io_temp_dir(&dirpath, scratch_pool));
    }

  switch (delete_when)
    {
      case svn_io_file_del_on_pool_cleanup:
        baton = apr_palloc(result_pool, sizeof(*baton));
        baton->pool = result_pool;
        baton->fname_apr = NULL;

        /* Because cleanups are run LIFO, we need to make sure to register
           our cleanup before the apr_file_close cleanup:

           On Windows, you can't remove an open file.
        */
        apr_pool_cleanup_register(result_pool, baton,
                                  temp_file_plain_cleanup_handler,
                                  temp_file_child_cleanup_handler);

        break;
      case svn_io_file_del_on_close:
        flags |= APR_DELONCLOSE;
        break;
      default:
        break;
    }

  SVN_ERR(temp_file_create(&tempfile, &tempname, dirpath, flags,
                           result_pool, scratch_pool));

#if !defined(WIN32) && !defined(__OS2__)
  /* apr_file_mktemp() creates files with mode 0600.
   * This is appropriate if we're using a system temp dir since we don't
   * want to leak sensitive data into temp files other users can read.
   * If we're not using a system temp dir we're probably using the
   * .svn/tmp area and it's likely that the tempfile will end up being
   * copied or renamed into the working copy.
   * This would cause working files having mode 0600 while users might
   * expect to see 0644 or 0664. So we tweak perms of the tempfile in this
   * case, but only if the umask allows it. */
  if (!using_system_temp_dir)
    {
      svn_error_t *err;

      SVN_ERR(merge_default_file_perms(tempfile, &perms, scratch_pool));
      err = file_perms_set2(tempfile, perms, scratch_pool);
      if (err)
        {
          if (APR_STATUS_IS_INCOMPLETE(err->apr_err) ||
              APR_STATUS_IS_ENOTIMPL(err->apr_err))
            svn_error_clear(err);
          else
            {
              return svn_error_quick_wrapf(
                       err, _("Can't set permissions on '%s'"),
                       svn_dirent_local_style(tempname, scratch_pool));
            }
        }
    }
#endif

  if (file)
    *file = tempfile;
  else
    SVN_ERR(svn_io_file_close(tempfile, scratch_pool));

  if (unique_path)
    *unique_path = tempname; /* Was allocated in result_pool */

  if (baton)
    SVN_ERR(cstring_from_utf8(&baton->fname_apr, tempname, result_pool));

  return SVN_NO_ERROR;
}

svn_error_t *
svn_io_file_readline(apr_file_t *file,
                     svn_stringbuf_t **stringbuf,
                     const char **eol,
                     svn_boolean_t *eof,
                     apr_size_t max_len,
                     apr_pool_t *result_pool,
                     apr_pool_t *scratch_pool)
{
  svn_stringbuf_t *str;
  const char *eol_str;
  apr_size_t numbytes;
  char c;
  apr_size_t len;
  svn_boolean_t found_eof;

  str = svn_stringbuf_create_ensure(80, result_pool);

  /* Read bytes into STR up to and including, but not storing,
   * the next EOL sequence. */
  eol_str = NULL;
  numbytes = 1;
  len = 0;
  found_eof = FALSE;
  while (!found_eof)
    {
      if (len < max_len)
        SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes,
                                       &found_eof, scratch_pool));
      len++;
      if (numbytes != 1 || len > max_len)
        {
          found_eof = TRUE;
          break;
        }

      if (c == '\n')
        {
          eol_str = "\n";
        }
      else if (c == '\r')
        {
          eol_str = "\r";

          if (!found_eof && len < max_len)
            {
              apr_off_t pos;

              /* Check for "\r\n" by peeking at the next byte. */
              pos = 0;
              SVN_ERR(svn_io_file_seek(file, APR_CUR, &pos, scratch_pool));
              SVN_ERR(svn_io_file_read_full2(file, &c, sizeof(c), &numbytes,
                                             &found_eof, scratch_pool));
              if (numbytes == 1 && c == '\n')
                {
                  eol_str = "\r\n";
                  len++;
                }
              else
                {
                  /* Pretend we never peeked. */
                  SVN_ERR(svn_io_file_seek(file, APR_SET, &pos, scratch_pool));
                  found_eof = FALSE;
                  numbytes = 1;
                }
            }
        }
      else
        svn_stringbuf_appendbyte(str, c);

      if (eol_str)
        break;
    }

  if (eol)
    *eol = eol_str;
  if (eof)
    *eof = found_eof;
  *stringbuf = str;

  return SVN_NO_ERROR;
}
