/*
  sshfc_transfer.c

  Author: Sami Lehtinen <sjl@ssh.fi>

  Copyright (C) 2000 SSH Communications Security Oy, Espoo, Finland
  All rights reserved.

  Functions that perform the actual transfer.
 */

#include "sshincludes.h"
#include "sshfilecopy.h"
#include "sshfilecopyi.h"
#include "sshbuffer.h"
#include "sshtimemeasure.h"

#define SSH_DEBUG_MODULE "SshFCTransfer"

#define SSH_FCT_CONN_ERROR_STATE "fct_conn_error"
#define CURRENT_STATE_HOLDER gdata->current_state
#define SRC FALSE,gdata->source_conn_flags

#define SSH_FCT_CHECK_DEST_CONN                                         \
do                                                                      \
{                                                                       \
  tdata->dealing_with_source = FALSE;                                   \
  SSH_FC_CHECK_CONN(gdata->dest_conn_flags, SSH_FCT_CONN_ERROR_STATE);  \
} while(0)

#define SSH_FCT_CHECK_SRC_CONN                                           \
do                                                                       \
{                                                                        \
  tdata->dealing_with_source = TRUE;                                     \
  SSH_FC_CHECK_CONN(gdata->source_conn_flags, SSH_FCT_CONN_ERROR_STATE); \
} while(0)

/**********
 * ssh_file_copy_transfer_files() related stuff.
 **********/

typedef struct SshFileCopyTransferContextRec
{
  /* Source connection. */
  SshFileCopyConnection source;
  /* Source location. */
  SshDlList file_list;
  /* Destination connection. */
  SshFileCopyConnection destination;
  /* Destination location. (single file. Must be directory, if
     multiple files are transferred. */
  SshFileCopyLocation dest_location;
  /* Transfer attributes. */
  SshFileCopyTransferAttrs attrs;  

  /* Callback (and context for it), with which success or failure
     messages are returned to the application. */
  SshFileCopyTransferReadyCallback completion_callback;
  SshFileCopyTransferErrorCallback error_callback;
  SshFileCopyTransferProgressCallback progress_callback;
  SshFileCopyTransferAbortNotifyCallback abort_notify_cb;
  
  void *callback_context;

  SshFSM fsm;
  SshFSMThread main_thread;
  
  SshFSMCondition in_blocking;
  SshFSMCondition out_blocking;
  SshBuffer *transfer_buffer;
  SshFSMThread reader;
  SshFSMThread writer;
  Boolean fatal_error_during_transfer;
  
  SshFSMThread source_conn_thread;
  SshFSMThread dest_conn_thread;
  SshFSMCondition source_conn_waiting;
  SshFSMCondition dest_conn_waiting;
  SshFSMCondition conn_failed;
  SshUInt8 source_conn_flags;
  SshUInt8 dest_conn_flags;
  
  /* Timer for single transfers . */
  SshTimeMeasure transfer_timer;
  /* Timer for the whole transfer operation. */
  SshTimeMeasure total_transfer_timer;

  /* Which attributes can be safely set with setstat and fsetstat,
     without getting redundant error messages, which only serve to
     confuse the user. This is 0x10, if the flags haven't been tested
     yet. */
  unsigned int attributes_flags;
  char *after_check_state;
  char *current_state;
  unsigned int checking_flag;

  /* After this is set, no callback should be called by SshFileCopy,
     and all transfers should end, and the threads should be destroyed. */
  Boolean aborted;
  SshOperationHandle operation_handle;
  
  Boolean received_eof;
} *SshFileCopyTransferContext;

typedef struct TransferThreadContextRec
{
  /* Source connection. */
  SshFileCopyConnection source;
  SshFileCopyFile source_file;
  SshFileCopyFile source_dir;
  char *source_file_name;

  SshFileCopyFile current_dest_file;

  SshFileCopyFileListItem current_item;
  SshFileCopyLocation current_location;
  
  /* Thread pointers. */
  SshFSMThread parent;
  SshFSMThread child;
  
  /* Whether we are currently interested in the source or the
     destination file (during stat or remove etc.). TRUE if yes, FALSE
     otherwise. */
  Boolean dealing_with_source;

  /* Helper variable, when dealing with directories. (contains a state
     name) */
  char *after_lstat_src_dir_state;
  
  off_t read_bytes;

  off_t written_bytes;
  off_t write_pending;

  /* Operation handle for the ssh_file_client_* operations, so that we
     can abort them, if we get aborted. */
  SshOperationHandle op_handle;
  
} *TransferThreadContext;

void transfer_thread_destructor(void *tdata)
{
  TransferThreadContext thread_context = (TransferThreadContext) tdata;

  SSH_PRECOND(tdata);

  /* XXX More cases. */
  if (thread_context->op_handle)
    {
      ssh_operation_abort(thread_context->op_handle);
      thread_context->op_handle = NULL;
    }
  if (thread_context->parent)
    {
      TransferThreadContext parent_tdata;
      parent_tdata = ssh_fsm_get_tdata(thread_context->parent);
      parent_tdata->child = NULL;
    }
  
  if (thread_context->child)
    {
      TransferThreadContext child_tdata;
      child_tdata = ssh_fsm_get_tdata(thread_context->child);
      child_tdata->parent = NULL;
      ssh_fsm_kill_thread(thread_context->child);
      thread_context->child = NULL;
    }

  if (thread_context->parent)
    {
      TransferThreadContext parent_tdata;
      
      parent_tdata = ssh_fsm_get_tdata(thread_context->parent);
      parent_tdata->child = NULL;
    }

  ssh_xfree(thread_context->source_file_name);
  thread_context->source_file_name = NULL;
  
#if 0  
  if (thread_context->current_dest_file)
    {
      ssh_file_copy_file_destroy(thread_context->current_dest_file);
      thread_context->current_dest_file = NULL;
    }
#endif
}

SSH_FSM_STEP(transfer_set_next_source_item)
{
  TransferThreadContext thread_context;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_PRECOND(thread == gdata->main_thread);

  SSH_FCT_CHECK_SRC_CONN;

  SSH_DEBUG(3, ("Setting next source file item..."));

  if (ssh_dllist_is_current_valid(gdata->file_list))
    {
      tdata->child = SSH_FSM_FORK(sizeof(*thread_context),              \
                                  "fct_next_file",                      \
                                  NULL, transfer_thread_destructor);

      thread_context = ssh_fsm_get_tdata(tdata->child);
      memset(thread_context, 0, sizeof(*thread_context));
      thread_context->parent = thread;
      thread_context->current_item =
        (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);
      thread_context->current_location = thread_context->current_item->files;
      ssh_dllist_fw(gdata->file_list, 1);
    }
  else
    {
      SSH_DEBUG(3, ("No more source files in this list."));
      SSH_FSM_SET_NEXT("fct_transfer_done");
      return SSH_FSM_CONTINUE;
    }

  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(transfer_set_next_source)
{
  TransferThreadContext thread_context;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_PRECOND(tdata->current_location);
  SSH_PRECOND(tdata->current_location->file_list);

  SSH_FCT_CHECK_SRC_CONN;

  if (ssh_dllist_is_current_valid(tdata->current_location->file_list))
    {
      if (tdata->current_location->raw)
        {
          SSH_DEBUG(2, ("Source file is \"raw\", and it needs to "
                        "be parsed."));
          SSH_FSM_SET_NEXT("fct_parse_raw_source");
          return SSH_FSM_CONTINUE;
        }
      else
        {
          SshFileCopyFile file;
          SshFileAttributes attributes;
          
          file = (SshFileCopyFile) ssh_dllist_current
            (tdata->current_location->file_list);

          if ((attributes = ssh_file_copy_file_get_attributes(file)) != NULL)
            {
              if (attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS)
                {
                  if ((attributes->permissions & S_IFMT) == S_IFDIR)
                    {
                      if (!file->dir_entries && gdata->attrs->recurse_dirs)
                        {
                          ssh_fatal
                            ("transfer_set_next_source: "
                             "Directory recursion is enabled, "
                             "but directory %s hasn't been "
                             "recursed. Fatal error, submit "
                             "bug report (read README).",
                             ssh_file_copy_file_get_name(file));
                        }
                      
                      if (!file->dir_entries || !gdata->attrs->recurse_dirs)
                        {
                          char *error_message;
                          
                          /* Directory hasn't been recursed,
                             ie. recursing isn't enabled. */

                          ssh_dsprintf(&error_message, "File %s is a "
                                       "directory, and recursion is not "
                                       "enabled. ",
                                       ssh_file_copy_file_get_name(file));
                          
                          (*gdata->error_callback)(SSH_FC_ERROR,
                                                   error_message,
                                                   gdata->callback_context);
                          
                          ssh_xfree(error_message);
                          goto drop_file;
                        }
                      
                      tdata->child = SSH_FSM_FORK(sizeof(*thread_context),     \
                                                  "fct_mkdir",                 \
                                                  NULL,                        \
                                                  transfer_thread_destructor);

                      thread_context = ssh_fsm_get_tdata(tdata->child);
                      memset(thread_context, 0, sizeof(*thread_context));
                      thread_context->current_location = file->dir_entries;
                      thread_context->current_item = tdata->current_item;

                      /* We have to advance this here, because the child
                         doesn't have access to this list. */
                      ssh_dllist_fw(tdata->current_location->file_list, 1);

                      goto common_continue;
                    }
#ifdef S_IFLNK
                  else if ((attributes->permissions & S_IFMT) == S_IFLNK)
                    {
                      /* The file should stat()ted to see whether it
                         points to a regular file. */
                      SSH_DEBUG(3, ("File (%s) is a symbolic link.",    \
                                    ssh_file_copy_file_get_name         \
                                    (file)));
                      goto stat_file;
                    }
#endif /* S_IFLNK */
                  else if ((attributes->permissions & S_IFMT) != S_IFREG)
                    {
                      /* File is not a regular, symbolic link or a
                         directory. It should be discarded. */
                      char *error_message;
                      
                      /* Directory hasn't been recursed,
                         ie. recursing isn't enabled. */
                      
                      ssh_dsprintf(&error_message, "File %s not a "
                                   "regular file, directory or symlink.",
                                   ssh_file_copy_file_get_name(file));
                      
                      (*gdata->error_callback)(SSH_FC_ERROR,
                                               error_message,
                                               gdata->callback_context);
                      
                      ssh_xfree(error_message);
                      goto drop_file;
                    }
                }
              else
                {
                  /* File should be statted again. */
                  SSH_DEBUG(3, ("File's (%s) attributes don't contain " \
                                "permissions.",                         \
                                ssh_file_copy_file_get_name(file)));
                  goto stat_file;
                }
            }
          else
            {
              SSH_DEBUG(3, ("File (%s) doesn't contain attributes.",    \
                            ssh_file_copy_file_get_name(file)));
            stat_file:
              /* The file should be statted again to get the
                 attributes. */
              SSH_DEBUG(3, ("Commencing stat on file..."));

              SSH_FSM_SET_NEXT("fct_stat_next_file");
              if (!tdata->source)
                tdata->source = gdata->source;
              
              return SSH_FSM_CONTINUE;
            }
          
          
          SSH_DEBUG(2, ("Next source file is %s/%s .",              \
                        tdata->current_location->file->name,
                        file->name));
          tdata->child = SSH_FSM_FORK(sizeof(*thread_context),      \
                                      "fct_stat_source",            \
                                      NULL, transfer_thread_destructor);

          thread_context = ssh_fsm_get_tdata(tdata->child);
          memset(thread_context, 0, sizeof(*thread_context));
          thread_context->current_location = tdata->current_location;
          thread_context->current_item = tdata->current_item;

        common_continue:          
          
          thread_context->parent = thread;

          thread_context->source = gdata->source;
                    
          thread_context->source_file = ssh_file_copy_file_dup(file);

          if (tdata->source_file)
            {  
              char *temp_filename, *ph, *basedir;
              ph = (char *)ssh_file_copy_file_get_name(file);
              basedir = tdata->source_file ? (char *)ssh_file_copy_file_get_name
                (tdata->source_file) : "";              
              
              ssh_dsprintf(&temp_filename, "%s%s%s",
                           *basedir ? basedir : "" ,
                           *ph && *basedir ? "/" : "",
                           *ph ? ph : "");
                        
              ssh_file_copy_file_register_filename
                (thread_context->source_file, temp_filename);
            }
          
          if (tdata->source_dir)
            thread_context->source_dir = tdata->source_dir;
          else
            thread_context->source_dir = tdata->current_location->file;
          
          thread_context->current_dest_file = ssh_file_copy_file_allocate();

          return SSH_FSM_SUSPENDED;

        drop_file:
          ssh_dllist_fw(tdata->current_location->file_list, 1);
          return SSH_FSM_CONTINUE;
        }
    }
  else
    {
      if (ssh_dllist_length(tdata->current_location->file_list) < 1)
        {
          if (!gdata->attrs->recurse_dirs)
            {
              char *error_message;
              
              /* Directory hasn't been recursed,
                 ie. recursing isn't enabled. */
              
              ssh_dsprintf(&error_message, "File %s is a "
                           "directory, and recursion is not "
                           "enabled. ",
                           ssh_file_copy_file_get_name
                           (tdata->current_location->file));
              
              (*gdata->error_callback)(SSH_FC_ERROR,
                                       error_message,
                                       gdata->callback_context);
                          
              ssh_xfree(error_message);
            }
          else
            {
              SSH_TRACE(2, ("Directory %s was empty",           \
                            ssh_file_copy_file_get_name         \
                            (tdata->current_location->file)));
            }
        }

      SSH_DEBUG(4, ("Current location list is at it's end. Moving " \
                    "to next..."));
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      return SSH_FSM_CONTINUE;
    }
  
  SSH_NOTREACHED;
  return SSH_FSM_FINISH;
}

void transfer_stat_next_file_cb(SshFileClientError error,
                                  SshFileAttributes attributes,
                                  void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SshFileCopyFile file;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;
  
  file = ssh_dllist_current(tdata->current_location->file_list);

  SSH_DEBUG(4, ("Received error %d.", error));

  SSH_ASSERT(file);
  
  switch (error)
    {
    case SSH_FX_OK:
      /* XXX */
      if ((attributes->permissions & S_IFMT) == S_IFDIR && !file->dir_entries)
        {
          ssh_dsprintf(&error_string, "Symbolic link %s points to "
                       "directory; it is excluded from copying.",
                       ssh_file_copy_file_get_name(file));
          
          (*gdata->error_callback)(SSH_FC_OK, error_string,
                                   gdata->callback_context);
          ssh_dllist_fw(tdata->current_location->file_list, 1);
          ssh_xfree(error_string);
        }
      else
        {
          SSH_ASSERT(file);
          file->attributes = ssh_file_attributes_dup(attributes);
        }
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(file));
    error_common:
      /* Report error. */
      ssh_dllist_fw(tdata->current_location->file_list, 1);
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_stat_and_replace");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_stat_and_replace");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_stat_next_file)
{
  SshFileCopyFile file;
  char *temp_filename, *ph, *basedir;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);  

  SSH_FCT_CHECK_SRC_CONN;

  SSH_PRECOND(tdata->source);  

  file = ssh_dllist_current(tdata->current_location->file_list);

  SSH_ASSERT(file);

  if (tdata->source_file)
    {  
      ph = (char *)ssh_file_copy_file_get_name(file);
      basedir = tdata->source_file ? (char *)ssh_file_copy_file_get_name
        (tdata->source_file) : "";              
              
      ssh_dsprintf(&temp_filename, "%s%s%s",
                   *basedir ? basedir : "" ,
                   *ph && *basedir ? "/" : "",
                   *ph ? ph : "");
    }
  else
    {
      temp_filename = ssh_xstrdup(ssh_file_copy_file_get_name(file));
    }

  if (tdata->source_dir)
    basedir = (char *)ssh_file_copy_file_get_name(tdata->source_dir);
  else
    basedir =
      (char *)ssh_file_copy_file_get_name(tdata->current_location->file);
      
  ssh_dsprintf(&(tdata->source_file_name), "%s%s%s",
               *basedir ? basedir : "", *basedir ? "/" : "", temp_filename);

  ssh_xfree(temp_filename);
  
  if (file->attributes)
    {
      ssh_xfree(file->attributes);
      file->attributes = NULL;
    }
            
  SSH_DEBUG(3, ("Statting file %s...", tdata->source_file_name));

  SSH_FSM_SET_NEXT("fct_next_file");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat            \
                     (tdata->source->client, tdata->source_file_name,   \
                      transfer_stat_next_file_cb, thread));
}

void transfer_mkdir_cb(SshFileClientError error,
                       void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;
  
  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      SSH_DEBUG(3, ("Successfully created directory %s.",       \
                    ssh_file_copy_file_get_name                 \
                    (tdata->current_dest_file)));      
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;
    case SSH_FX_FAILURE:  
      /* When creating directories, this error occurs if the directory
         already existed. So, the destination directory is statted to
         see if it exists, to avoid giving the user false messages. */
      SSH_FSM_SET_NEXT("fct_lstat_dest_dir");
      break;      
    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_mkdir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_mkdir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_mkdir)
{
  char *temp_filename;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_updir");

  ssh_dsprintf(&temp_filename, "%s/%s",
               ssh_file_copy_file_get_name(gdata->dest_location->file),
               ssh_file_copy_file_get_name(tdata->source_file));
  
  ssh_file_copy_file_register_filename(tdata->current_dest_file,
                                       temp_filename);

  /* XXX if destination file is not a directory, it should've been
     detected before this. */
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_mkdir   \
                     (gdata->destination->client,               \
                      ssh_file_copy_file_get_name               \
                      (tdata->current_dest_file),               \
                      NULL,                                     \
                      transfer_mkdir_cb,                        \
                      thread));
}


void transfer_lstat_dest_dir_cb(SshFileClientError error,
                                SshFileAttributes attributes,
                                void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* File (dir) exists. */
      SSH_TRACE(2, ("Destination directory %s already existed.",        \
                    ssh_file_copy_file_get_name                         \
                    (tdata->current_dest_file)));
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;
    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_lstat_dest_dir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_lstat_dest_dir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_lstat_dest_dir)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_updir");

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_lstat   \
                     (gdata->destination->client,               \
                      ssh_file_copy_file_get_name               \
                      (tdata->current_dest_file),               \
                      transfer_lstat_dest_dir_cb,               \
                      thread));
}

SSH_FSM_STEP(transfer_updir)
{
  TransferThreadContext child_tdata;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;
  
  if (gdata->attrs->unlink_source && gdata->attrs->preserve_attributes &&
      !(tdata->source_file->attributes &&
        tdata->source_file->attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS &&
        tdata->source_file->attributes->flags & SSH_FILEXFER_ATTR_ACMODTIME &&
        tdata->source_file->attributes->flags & SSH_FILEXFER_ATTR_UIDGID))
    {
      ssh_xfree(tdata->after_lstat_src_dir_state);
      tdata->after_lstat_src_dir_state = ssh_xstrdup("fct_updir");
      SSH_FSM_SET_NEXT("fct_lstat_source_dir");
      return SSH_FSM_CONTINUE;
    }

  if (tdata->after_lstat_src_dir_state)
    {
      /* so that we don't end to an infinite loop. (lstat_src_dir
         checks for the existence of
         tdata->after_lstat_src_dir_state)*/
      ssh_xfree(tdata->after_lstat_src_dir_state);
      tdata->after_lstat_src_dir_state = NULL;
    }
  
  SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
  
  tdata->child = SSH_FSM_FORK(sizeof(*child_tdata),             \
                              "fct_next_file",                  \
                              NULL,                             \
                              transfer_thread_destructor);  
  
  child_tdata = ssh_fsm_get_tdata(tdata->child);
  memset(child_tdata, 0, sizeof(*child_tdata));

  *child_tdata = *tdata;

  child_tdata->source_file = ssh_file_copy_file_dup(tdata->source_file);
  if (tdata->source_file_name)
    child_tdata->source_file_name = ssh_xstrdup(tdata->source_file_name);
  child_tdata->current_dest_file =
    ssh_file_copy_file_dup(tdata->current_dest_file);
  
  child_tdata->child = NULL;
  child_tdata->parent = thread;
  
  return SSH_FSM_SUSPENDED;
}

void transfer_chmod_destdir_cb(SshFileClientError error,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;
  
  if (gdata->aborted)
    return;
  
  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* XXX */
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied. (probably "
                   "failed to set some attributes)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_chmod_destdir)
{
  SshFileAttributes attrs;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_remove_source_dir");

  if (!gdata->attrs->preserve_attributes)
    {
      return SSH_FSM_CONTINUE;
    }

  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);
  
  if (!(attrs && attrs->flags & SSH_FILEXFER_ATTR_UIDGID &&
        attrs->flags & SSH_FILEXFER_ATTR_PERMISSIONS &&
        attrs->flags & SSH_FILEXFER_ATTR_ACMODTIME))
    {
      /* Attrs doesn't exist, or it isn't complete. We have to lstat
         the file again. */
      SSH_FSM_SET_NEXT("fct_lstat_source_dir");
      return SSH_FSM_CONTINUE;
    }  

  if (gdata->attributes_flags != 0x10)
    attrs->flags = gdata->attributes_flags;
  /* Remove size from flags. */
  attrs->flags &= ~SSH_FILEXFER_ATTR_SIZE;
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_setstat \
                     (gdata->destination->client,               \
                      ssh_file_copy_file_get_name               \
                      (tdata->current_dest_file),               \
                      attrs,                                    \
                      transfer_chmod_destdir_cb,                \
                      thread));
}

void transfer_lstat_src_dir_cb(SshFileClientError error,
                               SshFileAttributes attributes,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* XXX */
      ssh_file_copy_file_register_attributes
        (tdata->source_file,
         ssh_file_attributes_dup(attributes));
      
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      /* This is perfectly normal, if this is done after the
         removal. */
      if (gdata->attrs->unlink_source)
        {
          /* This should not happen, but here is the code for the
             situation anyway. */
          SSH_FSM_SET_NEXT("fct_remove_source_dir");
          ssh_dsprintf(&error_string, "%s: Couldn't set attributes.",
                       ssh_file_copy_file_get_name(tdata->source_file));
          error_code = SSH_FC_ERROR;
          
          (*gdata->error_callback)(error_code, error_string,
                                   gdata->callback_context);
          ssh_xfree(error_string);
          break;
        }
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->source_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(tdata->source_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      /* XXX This error occurs also when the destination directory
         already exits. This condition should be destected somehow, as
         it is not an error to copy identical directory structure on
         top of each other. (cp doesn't complain) */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->source_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_lstat_source_dir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_lstat_source_dir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_lstat_src_dir)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;

  ssh_xfree(tdata->source_file_name);
  
  {
    const char *basedir;
    
    basedir = ssh_file_copy_file_get_name(tdata->source_dir);
    ssh_dsprintf(&(tdata->source_file_name), "%s%s%s",
                 basedir[0] ? basedir : "",
                 basedir[0] ? "/" : "",
                 ssh_file_copy_file_get_name(tdata->source_file));
  }

  if (tdata->after_lstat_src_dir_state)
    {
      SSH_FSM_SET_NEXT(tdata->after_lstat_src_dir_state);
    }
  else
    {
      SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
    }
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_lstat   \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      transfer_lstat_src_dir_cb,                \
                      thread));
}

void transfer_rm_src_dir_cb(SshFileClientError error,
                            void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      SSH_DEBUG(2, ("Successfully removed source directory %s",         \
                    ssh_file_copy_file_get_name(tdata->source_file)));
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->source_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(tdata->source_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      /* XXX This error occurs also when the destination directory
         already exits. This condition should be destected somehow, as
         it is not an error to copy identical directory structure on
         top of each other. (cp doesn't complain) */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->source_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_remove_source_dir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_remove_source_dir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_rm_src_dir)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;

  SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
  
  if (!gdata->attrs->unlink_source)
    {
      return SSH_FSM_CONTINUE;
    }

  if (!tdata->source_file_name)
    {
      const char *basedir;

      basedir = ssh_file_copy_file_get_name(tdata->source_dir);
      ssh_dsprintf(&(tdata->source_file_name), "%s%s%s",
                   basedir[0] ? basedir : "",
                   basedir[0] ? "/" : "",
                   ssh_file_copy_file_get_name(tdata->source_file));
    }
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_rmdir   \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      transfer_rm_src_dir_cb,                   \
                      thread));
}

SSH_FSM_STEP(transfer_wake_parent_and_finish)
{
  SSH_FSM_TDATA(TransferThreadContext);

  if (tdata->parent)
    ssh_fsm_continue(tdata->parent);

  return SSH_FSM_FINISH;
}

void connection_wait_timeout(void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);  
}

void transfer_parse_stat_cb(SshFileClientError error,
                            SshFileAttributes attributes,
                            void *context)
{
  SshFileCopyFileListItem item;
  SshFileCopyFile file;
  SshFileCopyLocation to_be_deleted;
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  item = tdata->current_item;

  SSH_ASSERT(item);
  SSH_ASSERT(item->files->raw);
  
  SSH_ASSERT(ssh_dllist_is_current_valid(item->files->file_list));
  file = (SshFileCopyFile) ssh_dllist_current(item->files->file_list);

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Drop raw location, and make a new one. Put it back to the
         list in the place of the current one. */
      /* XXX This probably needs clean-up. Works now, though...*/
      to_be_deleted = item->files;

      item->files = ssh_file_copy_location_allocate(TRUE);
      
      ssh_file_copy_location_add_file(item->files,
                                      attributes,
                                      file->name);

      ssh_dllist_rewind(item->files->file_list);
      ssh_file_copy_location_destroy(to_be_deleted);
      tdata->current_location = item->files;
      
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   file->name);
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.", file->name);
      goto error_common;      
    case SSH_FX_FAILURE:
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   file->name);
    error_common:
      /* Some error occurred. Drop file, and report an error. */
      /* Report error. */
      ssh_dllist_fw(tdata->current_location->file_list, 1);
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:  
      SSH_FSM_SET_NEXT("fct_parse_raw_source");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_parse_raw_source");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);


      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }  

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_parse_raw_source)
{
  SshFileCopyFile file;

  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_PRECOND(tdata->current_location->raw);
  SSH_PRECOND(tdata->current_location->source);  

  SSH_FCT_CHECK_SRC_CONN;
  
  SSH_FSM_SET_NEXT("fct_next_file");
  
  /* stat file */  
  SSH_ASSERT(ssh_dllist_is_current_valid                \
             (tdata->current_location->file_list));

  file = (SshFileCopyFile) ssh_dllist_current
    (tdata->current_location->file_list);

  SSH_ASSERT(file);

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat    \
                     (gdata->source->client,                    \
                      ssh_file_copy_file_get_name(file),        \
                      transfer_parse_stat_cb,                   \
                      thread));
}

void transfer_stat_cb(SshFileClientError error,
                      SshFileAttributes attributes,
                      void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string = NULL;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  /* Rewrite this to handle errors in the "offcial" way. */
  switch (error)
    {
    case SSH_FX_OK:
      /* Store attributes */
      if (tdata->dealing_with_source)
        tdata->source_file->attributes = ssh_file_attributes_dup(attributes);
      else
        tdata->current_dest_file->attributes =
          ssh_file_attributes_dup(attributes);
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: File didn't exist.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
      break;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   ssh_file_copy_file_get_name(tdata->source_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->source_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_stat_source");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_stat_source");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_stat_source)
{
  char *basedir;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;
  
  if (gdata->dest_location->file->attributes == NULL)
    SSH_FSM_SET_NEXT("fct_stat_dest_before_remove");
  else
    SSH_FSM_SET_NEXT("fct_remove_dest");

  basedir = (char *)ssh_file_copy_file_get_name(tdata->source_dir);

  ssh_xfree(tdata->source_file_name);
  tdata->source_file_name = NULL;
  
  ssh_dsprintf(&(tdata->source_file_name), "%s%s%s",
               *basedir ? basedir : "", *basedir ? "/" : "",
               ssh_file_copy_file_get_name(tdata->source_file));

  /* If file has attributes, move to next state. */
  if (tdata->source_file->attributes == NULL ||
      !(tdata->source_file->attributes->flags &
        SSH_FILEXFER_ATTR_SIZE) ||
      !(tdata->source_file->attributes->flags &
        SSH_FILEXFER_ATTR_PERMISSIONS) ||
      ((gdata->attrs->preserve_attributes == TRUE) &&
       (!(tdata->source_file->attributes->flags &
          SSH_FILEXFER_ATTR_ACMODTIME) ||
        !(tdata->source_file->attributes->flags &
          SSH_FILEXFER_ATTR_UIDGID))) 
#ifdef S_IFLNK
          || ((tdata->source_file->attributes->permissions & S_IFMT) == S_IFLNK)
#endif /* S_IFLNK */
          )
    {
      tdata->dealing_with_source = TRUE;

      if (tdata->source_file->attributes)
        {
          ssh_xfree(tdata->source_file->attributes);
          tdata->source_file->attributes = NULL;
        }
            
      SSH_DEBUG(3, ("Statting source file %s...", tdata->source_file_name));
      
      SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat        \
                         (tdata->source->client,                        \
                          tdata->source_file_name,                      \
                          transfer_stat_cb, thread));
    }
  else
    {
      return SSH_FSM_CONTINUE;
    }
}

void transfer_stat_before_rm_cb(SshFileClientError error,
                                SshFileAttributes attributes,
                                void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Store attributes */
      ssh_file_copy_file_register_attributes
        (gdata->dest_location->file,
         ssh_file_attributes_dup(attributes));
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      /* Doesn't really matter. */
      SSH_DEBUG(2, ("Destination file didn't exist."));
      gdata->dest_location->file->attributes = NULL;
      break;
    case SSH_FX_PERMISSION_DENIED:
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied. (probably "
                   "failed to set some attributes)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_stat_dest_before_remove");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_stat_dest_before_remove");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_stat_dest_before_rm)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;      

  SSH_FSM_SET_NEXT("fct_remove_dest");

  SSH_DEBUG(3, ("Statting destination file %s before trying to remove it...", \
                gdata->dest_location->file->name));

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat    \
                     (gdata->destination->client,               \
                      gdata->dest_location->file->name,         \
                      transfer_stat_before_rm_cb, thread));
}

SSH_FSM_STEP(transfer_stat_dest)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;
  
  SSH_FSM_SET_NEXT("fct_chmod_dest_before");
  
  SSH_DEBUG(3, ("Statting destination file %s...",      \
                gdata->dest_location->file->name));

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat    \
                     (gdata->destination->client,               \
                       tdata->current_dest_file->name,          \
                       transfer_stat_cb, thread));
}

void transfer_remove_dest_cb(SshFileClientError error,
                             void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Everything OK. */
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      SSH_TRACE(2, ("File didn't previously exist"));
      break;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: No privileges to remove "
                   "destination file.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_remove_dest");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_remove_dest");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_rm_dest)
{
  char *temp_filename;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_open_dest");
  
  if (gdata->attrs->destination_must_be_dir &&
      (!gdata->dest_location->file->attributes ||
       ((gdata->dest_location->file->attributes->permissions & S_IFMT)
        != S_IFDIR)))
    {
      char *error_message;
      
      ssh_dsprintf(&error_message, "destination (%s) is not a directory.",
                   gdata->dest_location->file->name);
      
      (*gdata->completion_callback)(SSH_FC_ERROR_DEST_NOT_DIR,
                                    error_message,
                                    gdata->callback_context);

      ssh_xfree(error_message);
      /* XXX Kill other threads, etc. etc. */
      return SSH_FSM_FINISH;
    }

  if (gdata->attrs->destination_must_be_dir ||
      (gdata->dest_location->file->attributes &&
       ((gdata->dest_location->file->attributes->permissions & S_IFMT)
        == S_IFDIR)))
    ssh_dsprintf(&temp_filename, "%s/%s",
                 gdata->dest_location->file->name, tdata->source_file->name);
  else
    temp_filename = ssh_xstrdup(gdata->dest_location->file->name);
  
  ssh_file_copy_file_register_filename(tdata->current_dest_file,
                                       temp_filename);
  
  if (gdata->dest_location->file->attributes == NULL)
    {
      SSH_DEBUG(3, ("Skipping remove of destination file "      \
                    "(It doesn't exist)."));
      return SSH_FSM_CONTINUE;
    }
  
  SSH_DEBUG(2, ("Removing destination file %s .",      \
                tdata->current_dest_file->name));
  
  tdata->dealing_with_source = FALSE;
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_remove  \
                     (gdata->destination->client,               \
                      tdata->current_dest_file->name,           \
                      transfer_remove_dest_cb,                  \
                      thread));
}

void transfer_remove_source_cb(SshFileClientError error,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));
  
  switch (error)
    {
    case SSH_FX_OK:
      /* Everything OK. */
      SSH_DEBUG(3, ("Source file %s removed successfully.",     \
                    tdata->source_file_name));
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;
    case SSH_FX_NO_SUCH_FILE:
      ssh_warning("SshFileCopyFile: internal error; when removing "
                  "source file %s, received error SSH_FX_NO_SUCH_FILE, "
                  "even though source should exist, as it has presumably "
                  "been copied. Report bug and conditions to "
                  "ssh2-bugs@ssh.fi", tdata->source_file_name);
      break;
      /* Non-fatal errors. */
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: No privileges to remove "
                   "source file.",
                   tdata->source_file_name);
      goto error_common;      
    case SSH_FX_FAILURE:  
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   tdata->source_file_name);
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_remove_source");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_remove_source");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_rm_source)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;

  SSH_FSM_SET_NEXT("fct_close_dest");
  
  if (!gdata->attrs->unlink_source)
    {
      return SSH_FSM_CONTINUE;
    }
  
  tdata->dealing_with_source = TRUE;

  SSH_TRACE(2, ("Removing source file %s...", tdata->source_file_name));
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_remove  \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      transfer_remove_source_cb,                \
                      thread));
}

void transfer_open_cb(SshFileClientError error, SshFileHandle handle,
                      void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Store handle */
      SSH_DEBUG(4, ("File opened successfully."));
      if (tdata->dealing_with_source)
        tdata->source_file->handle = handle;
      else
        tdata->current_dest_file->handle = handle;
      break;
    case SSH_FX_EOF:
      SSH_NOTREACHED;
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s (%s): No such file or directory.",
                   tdata->dealing_with_source ?
                   tdata->source_file_name :
                   ssh_file_copy_file_get_name(tdata->current_dest_file),
                   tdata->dealing_with_source ? "source" : "dest");
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s (%s): Permission denied.",
                   tdata->dealing_with_source ?
                   tdata->source_file_name :
                   ssh_file_copy_file_get_name(tdata->current_dest_file),
                   tdata->dealing_with_source ? "source" : "dest");
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s (%s): Undefined error occurred.",
                   tdata->dealing_with_source ?
                   tdata->source_file_name :
                   ssh_file_copy_file_get_name(tdata->current_dest_file),
                   tdata->dealing_with_source ? "source" : "dest");
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);

      /* Skip file. */
      ssh_dllist_fw(tdata->current_location->file_list, 1);

      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");      
      break;
    case SSH_FX_NO_CONNECTION:
      if (tdata->dealing_with_source)
        {
          SSH_FSM_SET_NEXT("fct_open_source");
        }
      else
        {
          SSH_FSM_SET_NEXT("fct_open_dest");
        }
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      if (tdata->dealing_with_source)
        {
          SSH_FSM_SET_NEXT("fct_open_source");
          SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
          gdata->source_conn_flags = SSH_FCC_DOWN;
        }
      else
        {
          SSH_FSM_SET_NEXT("fct_open_dest");
          SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
          gdata->dest_conn_flags = SSH_FCC_DOWN;
        }      
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_open_dest)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_DEBUG(4, ("Opening file %s...",                                    \
                ssh_file_copy_file_get_name(tdata->current_dest_file)));

  SSH_FSM_SET_NEXT("fct_open_source");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_open    \
                     (gdata->destination->client,               \
                      tdata->current_dest_file->name,           \
                      O_WRONLY | O_CREAT | O_TRUNC,             \
                      NULL,                                     \
                      transfer_open_cb,                         \
                      thread));
}

SSH_FSM_STEP(transfer_open_source)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;

  SSH_DEBUG(4, ("Opening file %s...", tdata->source_file_name));
  
  SSH_FSM_SET_NEXT("fct_stat_dest");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_open    \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      O_RDONLY,                                 \
                      NULL,                                     \
                      transfer_open_cb,                         \
                      thread));
}

void transfer_chmod_dest_before_cb(SshFileClientError error,
                                   void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string = NULL;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  switch (error)
    {
    case SSH_FX_OK:
      /* XXX */
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied. (probably "
                   "failed to set some attributes)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_FAILURE:  
      /* XXX This happens almost every time. The user shouldn't see this,
         if it isn't for real. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_chmod_dest_dir");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_chmod_dest_before_transfer)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  struct SshFileAttributesRec attrs;

  SSH_FCT_CHECK_DEST_CONN;  

  memset(&attrs, 0, sizeof(attrs));
  attrs.flags = SSH_FILEXFER_ATTR_PERMISSIONS;
  attrs.permissions = 0000;

  SSH_FSM_SET_NEXT("fct_start_transfer");

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      &attrs,                                            \
                      transfer_chmod_dest_before_cb,                    \
                      thread));
}

void progress_timeout_cb(void *context)
{
  SshFileCopyTransferContext gdata = (SshFileCopyTransferContext) context;
  TransferThreadContext reader_tdata;
  TransferThreadContext writer_tdata;
  SshUInt64 seconds;
  
  if (gdata->aborted)
    return;

  reader_tdata = gdata->reader ? ssh_fsm_get_tdata(gdata->reader) : NULL;
  writer_tdata = ssh_fsm_get_tdata(gdata->writer);
  
  ssh_time_measure_get_value(gdata->transfer_timer,
                             &seconds, NULL);
  (*gdata->progress_callback)(writer_tdata->source,
                              writer_tdata->source_file,
                              gdata->destination,
                              writer_tdata->current_dest_file,
                              reader_tdata ?
                              reader_tdata->read_bytes
                              : writer_tdata->written_bytes,
                              writer_tdata->written_bytes,
                              seconds,
                              gdata->callback_context);

  ssh_register_timeout(1L, 0L, progress_timeout_cb, gdata);  
}

SSH_FSM_STEP(transfer_start)
{
  TransferThreadContext reader_tdata, writer_tdata;
  
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  /* Allocate buffers etc. */
  if (!gdata->in_blocking)
    gdata->in_blocking = ssh_fsm_condition_create(gdata->fsm);
  if (!gdata->out_blocking)
    gdata->out_blocking = ssh_fsm_condition_create(gdata->fsm);

  if (gdata->transfer_buffer)
    ssh_buffer_clear(gdata->transfer_buffer);
  else
    gdata->transfer_buffer = ssh_buffer_allocate();

  SSH_TRACE(2, ("Starting transfer for file %s, destination %s",        \
                tdata->source_file_name,                                \
                tdata->current_dest_file->name));
  
  gdata->reader = SSH_FSM_FORK(sizeof(*reader_tdata),   \
                               "fct_read_in",           \
                               NULL,                    \
                               transfer_thread_destructor);

  ssh_fsm_set_thread_name(gdata->reader,
                          "reader");

  gdata->writer = SSH_FSM_FORK(sizeof(*writer_tdata),   \
                               "fct_write_out",         \
                               NULL,                    \
                               transfer_thread_destructor);

  ssh_fsm_set_thread_name(gdata->writer,
                          "writer");

  reader_tdata = ssh_fsm_get_tdata(gdata->reader);
  writer_tdata = ssh_fsm_get_tdata(gdata->writer);

  memset(reader_tdata, 0, sizeof(*reader_tdata));
  memset(writer_tdata, 0, sizeof(*writer_tdata));

  gdata->received_eof = FALSE;

  *reader_tdata = *writer_tdata = *tdata;
  
  reader_tdata->dealing_with_source = TRUE;
  writer_tdata->dealing_with_source = FALSE;
  if (tdata->source_file_name)
    {
      reader_tdata->source_file_name = ssh_xstrdup(tdata->source_file_name);
      writer_tdata->source_file_name = ssh_xstrdup(tdata->source_file_name);
    }
  
  writer_tdata->source = FALSE;

  reader_tdata->read_bytes = 0L;
  writer_tdata->written_bytes = 0L;

  writer_tdata->write_pending = 0L;

  reader_tdata->parent = writer_tdata->parent = thread;
  
  if (gdata->progress_callback)
    {
      ssh_time_measure_reset(gdata->transfer_timer);
      ssh_time_measure_start(gdata->transfer_timer);
  
      /* Initialize timeout for progress callback. */
      ssh_register_timeout(1L, 0L, progress_timeout_cb, gdata);
    }

  SSH_FSM_SET_NEXT("fct_chmod_dest_after");
  
  return SSH_FSM_SUSPENDED;
}

SSH_FSM_STEP(transfer_check_setstat_flags)
{
  SSH_FSM_GDATA(SshFileCopyTransferContext);
  SSH_FSM_SET_NEXT("fct_check_setstat_flags_size");
  gdata->attributes_flags = 0;
  return SSH_FSM_CONTINUE;
}

void transfer_check_setstat_stat_source_cb(SshFileClientError error,
                                           SshFileAttributes attributes,
                                           void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  switch (error)
    {
    case SSH_FX_OK:
      ssh_file_copy_file_register_attributes
        (tdata->source_file, ssh_file_attributes_dup(attributes));

      SSH_ASSERT(attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS);
      
      break;
    case SSH_FX_EOF:
      SSH_NOTREACHED;
      break;
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory. ",
                   tdata->source_file_name);
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.",
                   tdata->source_file_name);
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.",
                   tdata->source_file_name);
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      ssh_dllist_fw(tdata->current_location->file_list, 1);
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      break;
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);
      

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_check_setstat_stat_source");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_check_setstat_stat_source");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      gdata->source_conn_flags = SSH_FCC_DOWN;
      break;
    }  

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_check_setstat_stat_source)
{
  const char *basedir;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_PRECOND(tdata->source);

  SSH_FCT_CHECK_SRC_CONN;
  
  SSH_FSM_SET_NEXT("fct_check_setstat_flags_size");
  ssh_xfree(tdata->source_file_name);
  basedir = ssh_file_copy_file_get_name(tdata->source_dir);
  ssh_dsprintf(&tdata->source_file_name, "%s%s%s",
               basedir[0] ? basedir : "", basedir[0] ? "/" : "",
               ssh_file_copy_file_get_name(tdata->source_file));

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_stat    \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      transfer_check_setstat_stat_source_cb,    \
                      thread));
}

void transfer_check_setstat_flags_cb(SshFileClientError error,
                                     void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;
  
  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Everything OK. */
      gdata->attributes_flags |= gdata->checking_flag;
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;
    case SSH_FX_NO_SUCH_FILE:
      /* This should not happen, ever. */
      SSH_NOTREACHED;      
      break;
      /* Non-fatal errors. */
    case SSH_FX_PERMISSION_DENIED:
    case SSH_FX_FAILURE:  
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT(gdata->current_state);
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT(gdata->current_state);
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_check_setstat_flags_size)
{
  SshFileAttributes attrs;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_check_setstat_flags_uidgid");
  
  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);

  if (!attrs || (attrs->flags != (SSH_FILEXFER_ATTR_SIZE |
                                  SSH_FILEXFER_ATTR_UIDGID |
                                  SSH_FILEXFER_ATTR_PERMISSIONS |
                                  SSH_FILEXFER_ATTR_ACMODTIME)))
    {
      SSH_FSM_SET_NEXT("fct_check_setstat_stat_source");
      return SSH_FSM_CONTINUE;
    }

  attrs->flags = gdata->checking_flag = SSH_FILEXFER_ATTR_SIZE;

  gdata->current_state = "fct_check_setstat_flags_size";
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      attrs,                                            \
                      transfer_check_setstat_flags_cb,                  \
                      thread));
}

SSH_FSM_STEP(transfer_check_setstat_flags_uidgid)
{
  SshFileAttributes attrs;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  SSH_PRECOND(tdata->current_dest_file->handle);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_check_setstat_flags_permissions");

  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);

  attrs->flags = gdata->checking_flag = SSH_FILEXFER_ATTR_UIDGID;

  gdata->current_state = "fct_check_setstat_flags_size";

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      attrs,                                            \
                      transfer_check_setstat_flags_cb,                  \
                      thread));
}

SSH_FSM_STEP(transfer_check_setstat_flags_permissions)
{
  SshFileAttributes attrs;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  SSH_PRECOND(tdata->current_dest_file->handle);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_check_setstat_flags_acmodtime");

  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);

  attrs->flags = gdata->checking_flag = SSH_FILEXFER_ATTR_PERMISSIONS;

  gdata->current_state = "fct_check_setstat_flags_permissions";

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      attrs,                                            \
                      transfer_check_setstat_flags_cb,                  \
                      thread));
}

SSH_FSM_STEP(transfer_check_setstat_flags_acmodtime)
{
  SshFileAttributes attrs;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  SSH_PRECOND(tdata->current_dest_file->handle);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_FSM_SET_NEXT("fct_check_setstat_flags_done");

  if (ssh_file_client_get_version(gdata->destination->client) == 0)
    {
      /* If server is old, this can't test can't be trusted. So let's
         just go with this one. */
      gdata->attributes_flags |= SSH_FILEXFER_ATTR_ACMODTIME;
      return SSH_FSM_CONTINUE;
    }
  
  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);

  attrs->flags = gdata->checking_flag = SSH_FILEXFER_ATTR_ACMODTIME;

  gdata->current_state = "fct_check_setstat_flags_acmodtime";

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      attrs,                                            \
                      transfer_check_setstat_flags_cb,                  \
                      thread));
}

SSH_FSM_STEP(transfer_check_setstat_flags_done)
{
  SshFileAttributes attrs;
  char *error_message;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_PRECOND(tdata->current_dest_file->handle);

  SSH_FSM_SET_NEXT(gdata->after_check_state);

  attrs = ssh_file_copy_file_get_attributes(tdata->source_file);
  
  attrs->flags = gdata->attributes_flags;

  if (gdata->attributes_flags == (SSH_FILEXFER_ATTR_SIZE |
                                  SSH_FILEXFER_ATTR_UIDGID |
                                  SSH_FILEXFER_ATTR_PERMISSIONS |
                                  SSH_FILEXFER_ATTR_ACMODTIME))
    return SSH_FSM_CONTINUE;

  if (!gdata->attrs->preserve_attributes)
    return SSH_FSM_CONTINUE;
  
  ssh_dsprintf(&error_message,
               "Following attribute(s) can't be changed on "
               "the remote file(s); %s%s%s%s",
               (gdata->attributes_flags & SSH_FILEXFER_ATTR_SIZE ?
                "" : "size "),
               (gdata->attributes_flags & SSH_FILEXFER_ATTR_UIDGID ?
                "" : "uidgid "),
               (gdata->attributes_flags & SSH_FILEXFER_ATTR_PERMISSIONS ?
                "" : "permissions "),
               (gdata->attributes_flags & SSH_FILEXFER_ATTR_ACMODTIME ?
                "" : "acmodtime"));

  (*gdata->error_callback)(SSH_FC_OK, error_message, gdata->callback_context);

  ssh_xfree(error_message);
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(transfer_reopen_source)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_SRC_CONN;

  SSH_DEBUG(4, ("Re-opening file %s...", tdata->source_file_name));
  
  SSH_FSM_SET_NEXT("fct_read_in");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_open    \
                     (tdata->source->client,                    \
                      tdata->source_file_name,                  \
                      O_RDONLY,                                 \
                      NULL,                                     \
                      transfer_open_cb,                         \
                      thread));
}

void transfer_read_cb(SshFileClientError error,
                      const unsigned char *data,
                      size_t len,
                      void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(5, ("error %d.", error));
  
  switch (error)
    {
    case SSH_FX_OK:
      break;
    case SSH_FX_EOF:
      SSH_DEBUG(3, ("Received SSH_FX_EOF."));
      gdata->received_eof = TRUE;
      break;
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory. (all of "
                   "a sudden, during transfer)",
                   tdata->source_file_name);
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.(all of "
                   "a sudden, during transfer)", tdata->source_file_name);
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.(all of "
                   "a sudden, during transfer)",
                   tdata->source_file_name);
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      gdata->fatal_error_during_transfer = TRUE;
      ssh_fsm_continue(gdata->writer);
      ssh_fsm_kill_thread(gdata->reader);
      /* So that possible call to progress_callback won't crash. */
      gdata->reader = NULL;
      return;
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);
      

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    case SSH_FX_NO_CONNECTION:
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      gdata->source_conn_flags = SSH_FCC_DOWN;
      SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
      return;
    }  

  SSH_DEBUG(7, ("read in %d bytes", len));
  ssh_buffer_append(gdata->transfer_buffer, data, len);
  tdata->read_bytes += len;
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_readwrite_error)
{
  SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(transfer_read_in)
{
  off_t reading_len;
  
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  if (gdata->fatal_error_during_transfer)
    {
      /* So that possible call to progress_callback won't crash. */
      gdata->reader = NULL;
      return SSH_FSM_FINISH;
    }

  SSH_FCT_CHECK_SRC_CONN;

  /* XXX cleanup */
  if (ssh_buffer_len(gdata->transfer_buffer) > 0L)
    SSH_FSM_CONDITION_SIGNAL(gdata->out_blocking);

  if (SSH_FC_MAX_BUFFER_SIZE - ssh_buffer_len(gdata->transfer_buffer) <
      SSH_FC_READ_MAX)
    SSH_FSM_CONDITION_WAIT(gdata->in_blocking);

  SSH_ASSERT(tdata->read_bytes <= tdata->source_file->attributes->size);
  
  if (tdata->read_bytes >= tdata->source_file->attributes->size ||
      gdata->received_eof)
    {
      SSH_FSM_CONDITION_SIGNAL(gdata->out_blocking);
      gdata->reader = NULL;
      return SSH_FSM_FINISH;
    }
  
  reading_len = (SSH_FC_READ_MAX <
                 (tdata->source_file->attributes->size -
                  tdata->read_bytes) ? SSH_FC_READ_MAX :
                 (tdata->source_file->attributes->size -
                  tdata->read_bytes));

  SSH_DEBUG(8, ("starting read of %d bytes", reading_len));

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_read    \
                     (tdata->source_file->handle,               \
                      tdata->read_bytes,                        \
                      reading_len,                              \
                      transfer_read_cb,                         \
                      thread));
}

void transfer_chmod_dest_after_cb(SshFileClientError error,
                                   void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;
  
  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Everything OK. */
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

      /* Non-fatal errors. */
    case SSH_FX_NO_SUCH_FILE:
      /* This should not happen, ever. */
      SSH_NOTREACHED;
      break;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: No privileges to change attributes on "
                   "destination file. (this probably isn't anything, as "
                   "non-roots rarely can re-set files owner)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* If the server is of older breed, these warnings are probably
         bogus. (atleast if the server doesn't have futimes function,
         but has utimes function) */
      if (ssh_file_client_get_version(gdata->destination->client) == 0)
        break;

      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred. (probably "
                   "failed to set some attributes)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:
      SSH_FSM_SET_NEXT("fct_chmod_dest_after");
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_FSM_SET_NEXT("fct_chmod_dest_after");
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      break;
      
      /* Fatal errors. */
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_chmod_dest_before_reopen)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  SshFileAttributes attributes;

  SSH_FCT_CHECK_DEST_CONN;
  
  attributes = ssh_xcalloc(1, sizeof(*attributes));

  if (!(tdata->current_dest_file->attributes->flags &
        SSH_FILEXFER_ATTR_PERMISSIONS))
    {
      /* XXX permissions are not present. the destination file must be
         statted again. */
      SSH_NOTREACHED;
    }

  /* Umask. */
  attributes->permissions =
    tdata->current_dest_file->attributes->permissions;

  attributes->flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
  SSH_FSM_SET_NEXT("fct_reopen_dest");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_setstat \
                     (gdata->destination->client,               \
                      tdata->current_dest_file->name,           \
                      attributes,                               \
                      transfer_chmod_dest_after_cb,             \
                      thread));
}

SSH_FSM_STEP(transfer_reopen_dest)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  SSH_DEBUG(4, ("Re-opening file %s...",                                 \
                ssh_file_copy_file_get_name(tdata->current_dest_file)));

  SSH_FSM_SET_NEXT("fct_chmod_dest_after_reopen");
  
  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_open    \
                     (gdata->destination->client,               \
                      tdata->current_dest_file->name,           \
                      O_WRONLY | O_CREAT | O_TRUNC,             \
                      NULL,                                     \
                      transfer_open_cb,                         \
                      thread));
}

SSH_FSM_STEP(transfer_chmod_dest_after_reopen)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  SshFileAttributes attrs;

  SSH_FCT_CHECK_DEST_CONN;
  
  attrs = ssh_xcalloc(1, sizeof(*attrs));

  attrs->flags = SSH_FILEXFER_ATTR_PERMISSIONS;
  attrs->permissions = 0000;

  SSH_FSM_SET_NEXT("fct_write_out");

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      attrs,                                            \
                      transfer_chmod_dest_before_cb,                    \
                      thread));
}

void transfer_write_cb(SshFileClientError error,
                       void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyError error_code;
  char *error_string;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  
  tdata->op_handle = NULL;

  if (gdata->aborted)
    return;

  SSH_DEBUG(5, ("error %d.", error));
  switch (error)
    {
    case SSH_FX_OK:
      break;
    case SSH_FX_EOF:
      /* Atleast according to current revision of the
         sshfilexfer-protocol, this can't happen with write.*/
      SSH_NOTREACHED;
      break;
    case SSH_FX_NO_SUCH_FILE:
      error_code = SSH_FC_ERROR_NO_SUCH_FILE;
      ssh_dsprintf(&error_string, "%s: No such file or directory. (all of "
                   "a sudden, during transfer)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;
    case SSH_FX_PERMISSION_DENIED:
      error_code = SSH_FC_ERROR_PERMISSION_DENIED;
      ssh_dsprintf(&error_string, "%s: Permission denied.(all of "
                   "a sudden, during transfer)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
      goto error_common;      
    case SSH_FX_FAILURE:  
      /* Some error occurred. Drop file, and report an error. */
      error_code = SSH_FC_ERROR_FAILURE;
      ssh_dsprintf(&error_string, "%s: Undefined error occurred.(all of "
                   "a sudden, during transfer)",
                   ssh_file_copy_file_get_name(tdata->current_dest_file));
    error_common:
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      gdata->fatal_error_during_transfer = TRUE;
      ssh_cancel_timeouts(progress_timeout_cb, gdata);
      break;
    case SSH_FX_BAD_MESSAGE:
      /* XXX we should exit the state machine, close connections
         etc. */
      ssh_cancel_timeouts(progress_timeout_cb, gdata);

      (*gdata->completion_callback)(SSH_FC_ERROR_PROTOCOL_MISMATCH,
                                    "File transfer protocol mismatch.",
                                    gdata->callback_context);
      
      
      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    case SSH_FX_NO_CONNECTION:
      SSH_TRACE(2, ("No connection yet. Waiting..."));
      ssh_register_timeout(SSH_FC_CONNECTION_WAIT_TIMEOUT,
                           connection_wait_timeout,
                           (void *)thread);
      return;
    case SSH_FX_CONNECTION_LOST:
      SSH_TRACE(2, ("Connection down, re-establishing..."));
      gdata->dest_conn_flags = SSH_FCC_DOWN;
      SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
      return;
    }
  
  SSH_DEBUG(7, ("wrote %d bytes", tdata->write_pending));
  
  if (error == SSH_FX_OK)
    {
      ssh_buffer_consume(gdata->transfer_buffer, tdata->write_pending);
      tdata->written_bytes += tdata->write_pending;
    }
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(transfer_write_out)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FCT_CHECK_DEST_CONN;

  if (ssh_buffer_len(gdata->transfer_buffer) < SSH_FC_MAX_BUFFER_SIZE)
    SSH_FSM_CONDITION_SIGNAL(gdata->in_blocking);

  if (tdata->written_bytes >= tdata->source_file->attributes->size ||
      (ssh_buffer_len(gdata->transfer_buffer) == 0 &&
       gdata->received_eof == TRUE) || gdata->fatal_error_during_transfer)
    {
      if (gdata->progress_callback)
        {
          
          SshUInt64 seconds;      
          /* Unregister any progress indicator timeouts. Call the
             progress-callback directly to indicate that the
             transfer ended. */
          ssh_cancel_timeouts(progress_timeout_cb, gdata);
          ssh_time_measure_stop(gdata->transfer_timer);
          ssh_time_measure_get_value(gdata->transfer_timer,
                                     &seconds, NULL);
          (*gdata->progress_callback)(tdata->source,
                                      tdata->source_file,
                                      gdata->destination,
                                      tdata->current_dest_file,
                                      gdata->reader ?
                                      ((TransferThreadContext)
                                       ssh_fsm_get_tdata(gdata->reader))->
                                      read_bytes
                                      : tdata->written_bytes,
                                      tdata->written_bytes,
                                      seconds,
                                      gdata->callback_context);
        }      
      if (gdata->fatal_error_during_transfer)
        {
          ssh_fsm_set_next(tdata->parent, "fct_readwrite_error");
          ssh_fsm_continue(tdata->parent);
          ssh_dllist_fw(tdata->current_location->file_list, 1);
        }
      gdata->writer = NULL;
      SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
      return SSH_FSM_CONTINUE;
    }

  /* We write what we can to disk, if reader dies. We finish when the
     transfer_buffer is empty. */
  /* XXX Currently, if reader dies, gdata*/
  if (ssh_buffer_len(gdata->transfer_buffer) == 0 &&
      gdata->reader == NULL)
    {
      ssh_cancel_timeouts(progress_timeout_cb, gdata);
      ssh_time_measure_stop(gdata->transfer_timer);
      gdata->writer = NULL;
      return SSH_FSM_FINISH;
    }
  
  if (ssh_buffer_len(gdata->transfer_buffer) == 0)
    SSH_FSM_CONDITION_WAIT(gdata->out_blocking);

  SSH_ASSERT(tdata->written_bytes <= tdata->source_file->attributes->size);
  
  tdata->write_pending = (SSH_FC_READ_MAX <
                          ssh_buffer_len(gdata->transfer_buffer) ?
                          SSH_FC_READ_MAX :
                          ssh_buffer_len(gdata->transfer_buffer));
  
  SSH_DEBUG(8, ("starting write of %d bytes", tdata->write_pending));

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_write   \
                     (tdata->current_dest_file->handle,         \
                      tdata->written_bytes,                     \
                      ssh_buffer_ptr(gdata->transfer_buffer),   \
                      tdata->write_pending,                     \
                      transfer_write_cb,                        \
                      thread));
}

SSH_FSM_STEP(transfer_chmod_dest_after_transfer)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  struct SshFileAttributesRec attributes;

  SSH_FCT_CHECK_DEST_CONN;  

  if (gdata->attributes_flags == 0x10 && gdata->attrs->preserve_attributes)
    {
      gdata->after_check_state = ssh_xstrdup("fct_chmod_dest_after");
      SSH_FSM_SET_NEXT("fct_check_setstat_flags");
      return SSH_FSM_CONTINUE;
    }

  memset(&attributes, 0, sizeof(attributes));
  
  if (!(tdata->current_dest_file->attributes->flags &
        SSH_FILEXFER_ATTR_PERMISSIONS))
    {
      /* XXX permissions are not present. the destination file must be
         statted again. */
      SSH_NOTREACHED;
    }

  if (!(tdata->source_file->attributes->flags &
        SSH_FILEXFER_ATTR_PERMISSIONS))
    {
      /* XXX permissions are not present. the source file must be
         statted again. */
      SSH_NOTREACHED;
    }

  if (gdata->attrs->preserve_attributes)
    {
      if (gdata->attributes_flags & SSH_FILEXFER_ATTR_UIDGID)
        if (tdata->source_file->attributes->flags & SSH_FILEXFER_ATTR_UIDGID)
          {
            attributes.uid = tdata->source_file->attributes->uid;
            attributes.gid = tdata->source_file->attributes->gid;
            attributes.flags |= SSH_FILEXFER_ATTR_UIDGID;
          }

      if (gdata->attributes_flags & SSH_FILEXFER_ATTR_PERMISSIONS)
        if (tdata->source_file->attributes->flags &
            SSH_FILEXFER_ATTR_PERMISSIONS)
          {
            attributes.permissions =
              tdata->source_file->attributes->permissions;
            attributes.flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
          }

      if (gdata->attributes_flags & SSH_FILEXFER_ATTR_ACMODTIME)
        if (tdata->source_file->attributes->flags & SSH_FILEXFER_ATTR_ACMODTIME)
          {
            attributes.mtime = tdata->source_file->attributes->mtime;
            attributes.atime = tdata->source_file->attributes->atime;
            attributes.flags |= SSH_FILEXFER_ATTR_ACMODTIME;
          }
    }
  else
    {
      /* This has to be done anyway, so if these operations are not
         supported, it still valid to tell that to the user. */
      /* Umask. */
      attributes.permissions =
        tdata->current_dest_file->attributes->permissions;

      /* exec-bit. */
      attributes.permissions |=
        (tdata->source_file->attributes->permissions & 0111);

      attributes.flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
    }

  SSH_FSM_SET_NEXT("fct_close_source");

  SSH_FSM_ASYNC_CALL(tdata->op_handle = ssh_file_client_fsetstat        \
                     (tdata->current_dest_file->handle,                 \
                      &attributes,                                       \
                      transfer_chmod_dest_after_cb,                     \
                      thread));
}

SSH_FSM_STEP(transfer_close_src)
{
  SSH_FSM_TDATA(TransferThreadContext);

  SSH_TRACE(3, ("Closing source file %s...",                            \
                ssh_file_copy_file_get_name(tdata->source_file)));

  SSH_FSM_SET_NEXT("fct_remove_source");
  
  ssh_file_client_close(tdata->source_file->handle,
                        NULL,
                        NULL);

  tdata->source_file->handle = NULL;
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(transfer_close_dest)
{
  SSH_FSM_TDATA(TransferThreadContext);

  SSH_TRACE(3, ("Closing dest file %s...",              \
                ssh_file_copy_file_get_name             \
                (tdata->current_dest_file)));

  SSH_FSM_SET_NEXT("fct_one_transfer_done");

  ssh_file_client_close(tdata->current_dest_file->handle,
                        NULL,
                        NULL);

  tdata->current_dest_file->handle = NULL;
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(transfer_one_done)
{
  SSH_FSM_TDATA(TransferThreadContext);
  
  ssh_dllist_fw(tdata->current_location->file_list, 1);

  SSH_TRACE(1, ("Finished with file %s.",       \
                ssh_file_copy_file_get_name     \
                (tdata->current_dest_file)));
  
  if (tdata->current_dest_file)
    {
      ssh_file_copy_file_destroy(tdata->current_dest_file);
      tdata->current_dest_file = NULL;
    }
  if (tdata->source_file)
    {
      ssh_file_copy_file_destroy(tdata->source_file);
      tdata->source_file = NULL;
    }
  
  SSH_FSM_SET_NEXT("fct_wake_parent_and_finish");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(transfer_conn_error)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  SSH_FSM_SET_NEXT(gdata->current_state);
  
  switch (tdata->dealing_with_source ? gdata->source_conn_flags :
          gdata->dest_conn_flags)
    {
    case SSH_FCC_OK:
      break;
    case SSH_FCC_DOWN:
      SSH_TRACE(2, ("Not yet connected, or connection down, waiting..."));
      if (tdata->dealing_with_source)
        {
          SSH_FSM_THROW(gdata->source_conn_thread, SSH_FCC_CONN_DOWN);
          SSH_FSM_CONDITION_WAIT(gdata->source_conn_waiting);
        }
      else
        {
          SSH_FSM_THROW(gdata->dest_conn_thread, SSH_FCC_CONN_DOWN);
          SSH_FSM_CONDITION_WAIT(gdata->dest_conn_waiting);
        }
      break;
    case SSH_FCC_CONNECTION_FAILED:
      {
        char *error_message;

        if (tdata->dealing_with_source)
          {
            ssh_dsprintf(&error_message, "Connection to source failed "
                         "(host = %s, user = %s, port = %s).",
                         gdata->source->host,
                         gdata->source->user, gdata->source->port);          }
        else
          {
            ssh_dsprintf(&error_message, "Connection to destination failed "
                         "(host = %s, user = %s, port = %s).",
                         gdata->destination->host,
                         gdata->destination->user, gdata->destination->port);
          }
        
        /* Return error to application using supplied callback. */
        (*gdata->completion_callback)(SSH_FC_ERROR_CONNECTION_FAILED,
                                      error_message,
                                      gdata->callback_context);
            
        ssh_xfree(error_message);
      }
      /* XXX Kill threads, free memory, etc. */
      SSH_NOTREACHED;
    case SSH_FCC_ERROR:
      /* XXX */
      SSH_NOTREACHED;
    case SSH_FCC_ABORTED:
      /* XXX */
      SSH_NOTREACHED;
    }
  return SSH_FSM_CONTINUE;
}


typedef struct CompletionTimeoutCtxRec
{
  SshFileCopyTransferReadyCallback completion_callback;
  void *callback_context;
} *CompletionTimeoutCtx;

/* The calling of the completion callback from the bottom of the event
   loop is a fix for a persistent bug in Windows. */
void completion_timeout(void *context)
{
  CompletionTimeoutCtx ctx = (CompletionTimeoutCtx) context;

  SSH_PRECOND(ctx);
  
  (*ctx->completion_callback)(SSH_FC_OK, "", ctx->callback_context);

  memset(ctx, 'F', sizeof(*ctx));
  ssh_xfree(ctx);
}

SSH_FSM_STEP(transfer_done)
{
  CompletionTimeoutCtx completion_timeout_ctx = NULL;
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);

  if (tdata->child)
    {
      TransferThreadContext child_tdata;
      child_tdata = ssh_fsm_get_tdata(tdata->child);
      child_tdata->parent = NULL;
      ssh_fsm_kill_thread(tdata->child);
      tdata->child = NULL;
    }
  
  if (gdata->reader)
    {
      ssh_fsm_kill_thread(gdata->reader);
      gdata->reader = NULL;
    }
  
  if (gdata->writer)
    {
      ssh_fsm_kill_thread(gdata->writer);
      gdata->writer = NULL;
    }
  
  if (gdata->source_conn_thread)
    ssh_fsm_kill_thread(gdata->source_conn_thread);
  
  if (gdata->dest_conn_thread)
    ssh_fsm_kill_thread(gdata->dest_conn_thread);

  completion_timeout_ctx = ssh_xcalloc(1, sizeof(*completion_timeout_ctx));
  completion_timeout_ctx->completion_callback = gdata->completion_callback;
  completion_timeout_ctx->callback_context = gdata->callback_context;

  ssh_fsm_destroy(gdata->fsm);
  ssh_register_timeout(0L, 0L, completion_timeout, completion_timeout_ctx);
  
  return SSH_FSM_FINISH;
}

SSH_FSM_STEP(transfer_abort)
{
  SSH_FSM_DATA(SshFileCopyTransferContext, TransferThreadContext);
  /* Abort the transfer. */
  
  ssh_cancel_timeouts(progress_timeout_cb, SSH_ALL_CONTEXTS);

  if (gdata->main_thread == thread)
    gdata->main_thread = NULL;

  if (gdata->reader)
    ssh_fsm_kill_thread(gdata->reader);
  if (gdata->writer)
    ssh_fsm_kill_thread(gdata->writer);
  
  if (gdata->source_conn_thread)
    ssh_fsm_kill_thread(gdata->source_conn_thread);
  
  if (gdata->dest_conn_thread)
    ssh_fsm_kill_thread(gdata->dest_conn_thread);

  if (tdata->child)
    {
      TransferThreadContext child_tdata;
      child_tdata = ssh_fsm_get_tdata(tdata->child);
      child_tdata->parent = NULL;
      ssh_fsm_kill_thread(tdata->child);
      tdata->child = NULL;
    }
  
  gdata->operation_handle = NULL;
  ssh_fsm_destroy(gdata->fsm);

  if (gdata->abort_notify_cb)
    (*gdata->abort_notify_cb)(gdata->callback_context);

  return SSH_FSM_FINISH;
}

SshFSMStateMapItem transfer_states_array[] =
{ { "fct_stat_source", "Stat source file", transfer_stat_source },
  { "fct_stat_dest", "Stat destination file", transfer_stat_dest },
  { "fct_stat_dest_before_remove",
    "Stat destination file before it is removed.",
    transfer_stat_dest_before_rm },  
  { "fct_remove_dest", "Remove destination file", transfer_rm_dest },
  { "fct_remove_source", "Remove source file", transfer_rm_source },
  { "fct_open_dest", "Open destination file", transfer_open_dest },
  { "fct_open_source", "Open source file", transfer_open_source },
  { "fct_chmod_dest_before",
    "Change permissions of dest file before transfer",
    transfer_chmod_dest_before_transfer },
  { "fct_start_transfer", "Prepare everything for the actual transfer",
    transfer_start },
  
#include "sshfc_conn_states.h"

  /* The "real" transfer happens in these states. */
  { "fct_read_in", "Try to read file", transfer_read_in },
  { "fct_reopen_source", "Re-open file, in case connection is destroyed",
    transfer_reopen_source },
  { "fct_write_out", "Try to write data to file", transfer_write_out },
  { "fct_chmod_dest_before_reopen", "chmod dest before re-opening it",
    transfer_chmod_dest_before_reopen },
  { "fct_reopen_dest", "Re-open destination file", transfer_reopen_dest },
  { "fct_chmod_dest_after_reopen", "chmod dest after re-opening it",
    transfer_chmod_dest_after_reopen },
  { "fct_readwrite_error", "Un-recoverable error during actual transfer",
    transfer_readwrite_error },

  { "fct_mkdir", "Make a new directory", transfer_mkdir },
  { "fct_lstat_dest_dir", "lstat destination directory",
    transfer_lstat_dest_dir },
  { "fct_updir", "Start going through files of subdirectory", transfer_updir },
  { "fct_chmod_dest_dir",
    "Change permissions and fileattributes of destination directory",
    transfer_chmod_destdir },
  { "fct_remove_source_dir", "Remove source directory", transfer_rm_src_dir },
  { "fct_lstat_source_dir", "lstat source directory",
    transfer_lstat_src_dir },

  /* These states find out which attributes can be changed with
     setstat and fsetstat. */
  { "fct_check_setstat_flags",
    "Check which attributes can be changed with setstat",
    transfer_check_setstat_flags },
  { "fct_check_setstat_stat_source", "Stat source file",
    transfer_check_setstat_stat_source },
  { "fct_check_setstat_flags_size", "Can size be changed with setstat",
    transfer_check_setstat_flags_size },
  { "fct_check_setstat_flags_uidgid",
    "Can uid and gid be changed with setstat",
    transfer_check_setstat_flags_uidgid },
  { "fct_check_setstat_flags_permissions",
    "Can permissions be changed with setstat",
    transfer_check_setstat_flags_permissions },
  { "fct_check_setstat_flags_acmodtime",
    "Can access and modification times be changed with setstat",
    transfer_check_setstat_flags_acmodtime },
  { "fct_check_setstat_flags_done", "Check done",
    transfer_check_setstat_flags_done },
  
  { "fct_chmod_dest_after",
    "Change permissions of dest file after transfer",
    transfer_chmod_dest_after_transfer },
  { "fct_next_file_item", "Set next source file item",
    transfer_set_next_source_item },
  { "fct_next_file", "Set next source file",
    transfer_set_next_source },
  { "fct_stat_next_file", "Stat file during next_file",
    transfer_stat_next_file },
  { "fct_parse_raw_source", "Parse raw source location",
    transfer_parse_raw_source },
  { "fct_close_source", "Close source file", transfer_close_src },
  { "fct_close_dest", "Close destination file", transfer_close_dest },
  { "fct_one_transfer_done", "Finalize after transfer of one file",
    transfer_one_done },
  { "fct_transfer_done", "Transfer done", transfer_done },

  { "fct_conn_error", "Handle an error with a connection.",
    transfer_conn_error},
  { "fct_wake_parent_and_finish", "Wake parent and finish",
    transfer_wake_parent_and_finish },
  { "fct_abort", "Abort file transfer exchange", transfer_abort }
};

void *subdir_rewind(void *item, void *ctx)
{
  SshFileCopyFile file = (SshFileCopyFile) item;

  SSH_PRECOND(file);

  if (file->dir_entries)
    {
      ssh_dllist_mapcar(file->dir_entries->file_list,
                        subdir_rewind, NULL);
      ssh_dllist_rewind(file->dir_entries->file_list);
    }
  
  return item;
}

void *location_list_rewind(void *item, void *ctx)
{
  SshFileCopyFileListItem list_item = (SshFileCopyFileListItem) item;

  SSH_PRECOND(list_item->files->file_list);

  ssh_dllist_mapcar(list_item->files->file_list, subdir_rewind, NULL);
  ssh_dllist_rewind(list_item->files->file_list);

  return item;
}

void *subdir_count(void *item, void *ctx)
{
  SshFileCopyFile file = (SshFileCopyFile) item;
  int *count = (int *) ctx;

  SSH_PRECOND(file);
  SSH_PRECOND(count);

  if (file->dir_entries)
    {
      *count +=ssh_dllist_length(file->dir_entries->file_list);
    }
  
  return item;
}

void *location_list_count(void *item, void *ctx)
{
  SshFileCopyFileListItem list_item = (SshFileCopyFileListItem) item;
  int *count = (int *) ctx;
  
  SSH_PRECOND(list_item->files->file_list);
  SSH_PRECOND(count);
  
  *count +=ssh_dllist_length(list_item->files->file_list);

  ssh_dllist_mapcar(list_item->files->file_list, subdir_count, count);

  return item;
}

void *location_list_mapcar_subdir_rewind(void *item, void *ctx)
{
  SshFileCopyFile file = (SshFileCopyFile) item;
  
  SSH_PRECOND(file);

  if (strcmp("", ssh_file_copy_file_get_name(file)) == 0)
    return item;
  
  if (file->dir_entries)
    {
      ssh_dllist_rewind(file->dir_entries->file_list);
      ssh_dllist_mapcar(file->dir_entries->file_list,
                        location_list_mapcar_subdir_rewind, ctx);
    }
  
  return item;
}

void *location_list_rewind_mapcar(void *item, void *ctx)
{
  SshFileCopyFileListItem list_item = (SshFileCopyFileListItem) item;

  SSH_PRECOND(list_item->files->file_list);

  ssh_dllist_rewind(list_item->files->file_list);
  ssh_dllist_mapcar(list_item->files->file_list,
                    location_list_mapcar_subdir_rewind, ctx);
  
  return item;
}

void ssh_file_copy_file_list_rewind(SshDlList file_list)
{
  ssh_dllist_rewind(file_list);
  ssh_dllist_mapcar(file_list, location_list_rewind_mapcar, NULL);
}

typedef struct FileListMapcarContextRec
{
  SshFileCopyFileListMapCarFunc func;
  void *context;
} *FileListMapcarContext;

void *location_list_mapcar_subdir(void *item, void *ctx)
{
  SshFileCopyFile file = (SshFileCopyFile) item;
  FileListMapcarContext context = (FileListMapcarContext) ctx;
  
  SSH_PRECOND(file);
  SSH_PRECOND(context);
  SSH_PRECOND(context->func);

  if (strcmp("", ssh_file_copy_file_get_name(file)) != 0)
    (*context->func)(file, context->context);
  
  if (file->dir_entries)
    {
      ssh_dllist_mapcar(file->dir_entries->file_list,
                        location_list_mapcar_subdir, context);
    }
  
  return item;
}

void *location_list_mapcar(void *item, void *ctx)
{
  FileListMapcarContext context = (FileListMapcarContext) ctx;
  SshFileCopyFileListItem list_item = (SshFileCopyFileListItem) item;

  SSH_PRECOND(context);
  SSH_PRECOND(context->func);  
  SSH_PRECOND(list_item->files->file_list);

  if (list_item->files->file)
    (*context->func)(list_item->files->file, context->context);

  ssh_dllist_mapcar(list_item->files->file_list,
                    location_list_mapcar_subdir, context);
  
  return item;
}

void ssh_file_copy_file_list_mapcar(SshDlList file_list,
                                    SshFileCopyFileListMapCarFunc func,
                                    void *context)
{  
  FileListMapcarContext ctx;
  ctx = ssh_xcalloc(1, sizeof(*ctx));
  ctx->func = func;
  ctx->context = context;

  ssh_dllist_rewind(file_list);
  
  ssh_dllist_mapcar(file_list, location_list_mapcar, ctx);
}

void transfer_fsm_destructor(void *gdata)
{
  SshFileCopyTransferContext transfer_context =
    (SshFileCopyTransferContext) gdata;
  
  SSH_PRECOND(gdata);
  /* XXX */

  if (transfer_context->operation_handle)
    ssh_operation_unregister(transfer_context->operation_handle);
  
  if (transfer_context->transfer_buffer)
    {
      ssh_buffer_free(transfer_context->transfer_buffer);
      transfer_context->transfer_buffer = NULL;
    }
  if (transfer_context->in_blocking)
    {
      ssh_fsm_condition_destroy(transfer_context->in_blocking);
      transfer_context->in_blocking = NULL;
    }
  if (transfer_context->out_blocking)
    {
      ssh_fsm_condition_destroy(transfer_context->out_blocking);
      transfer_context->out_blocking = NULL;
    }
  ssh_time_measure_free(transfer_context->transfer_timer);
  ssh_time_measure_free(transfer_context->total_transfer_timer);
}

void mapcar_count_func(SshFileCopyFile file, void *context)
{
  int *count = (int *) context;
  SSH_PRECOND(context);
  SSH_PRECOND(file);
  (*count)++;
}

void transfer_operation_abort_cb(void *operation_context)
{
  SshFileCopyTransferContext gdata =
    (SshFileCopyTransferContext) operation_context;

  gdata->aborted = TRUE;
  ssh_fsm_set_next(gdata->main_thread, "fct_abort");
  ssh_fsm_continue(gdata->main_thread);
}

SshOperationHandle
ssh_file_copy_transfer_files(SshFileCopyConnection source,
                             SshDlList file_list,
                             SshFileCopyConnection destination,
                             SshFileCopyLocation dest_location,
                             SshFileCopyTransferAttrs attrs,
                             SshFileCopyTransferReadyCallback ready_cb,
                             SshFileCopyTransferErrorCallback error_cb,
                             SshFileCopyTransferProgressCallback
                             progress_cb,
                             SshFileCopyTransferAbortNotifyCallback
                             abort_notify_cb,
                             void *context)
{
  SshFileCopyTransferContext transfer_context;
  TransferThreadContext tdata;
  
  SshFSM fsm;
  int count;
  
  SSH_PRECOND(source);
  SSH_PRECOND(file_list);
  SSH_PRECOND(destination);
  SSH_PRECOND(dest_location);
  SSH_PRECOND(ready_cb);
  SSH_PRECOND(error_cb);
  
  /* Count files in file_list (if more than one, destination must
     be directory). */
  count = 0;
  ssh_file_copy_file_list_mapcar(file_list,
                                 mapcar_count_func,
                                 &count);

  
  SSH_DEBUG(2, ("File list has %d files.", count));

  if (count <= 0)
    {
      (*ready_cb)(SSH_FC_OK, "No files to transfer.", context);
      return NULL;
    }

  if (attrs == NULL)
    {
      /* Use default attrs. */
      attrs = ssh_xcalloc(1, sizeof(*attrs));
    }

  if (count > 2)
    {
      /* Ignore the first file items basedir. If we have multiple
         file_items, this doesn't matter, as then we have multiple
         files, too. */
      attrs->destination_must_be_dir = TRUE;
    }
  
  fsm = ssh_fsm_allocate(sizeof(*transfer_context),
                         transfer_states_array,
                         SSH_FSM_NUM_STATES(transfer_states_array),
                         transfer_fsm_destructor);

  transfer_context = ssh_fsm_get_gdata_fsm(fsm);
  memset(transfer_context, 0, sizeof(*transfer_context));

  /* This is a "deadbeef" value, ie. with this flag, no attributes
     will be preserved. This is also a marker for the state machine,
     that the real value hasn't been queried yet. */
  transfer_context->attributes_flags = (unsigned int) 0x10;
  
  transfer_context->source = source;
  transfer_context->file_list = file_list;
  transfer_context->destination = destination;
  transfer_context->dest_location = dest_location;
  transfer_context->attrs = attrs;
  transfer_context->completion_callback = ready_cb;
  transfer_context->error_callback = error_cb;
  transfer_context->progress_callback = progress_cb;
  transfer_context->abort_notify_cb = abort_notify_cb;
  transfer_context->callback_context = context;
  transfer_context->fsm = fsm;
    
  /* Rewind all source file lists. */
  ssh_file_copy_file_list_rewind(file_list);
  
  transfer_context->conn_failed =
    ssh_fsm_condition_create(transfer_context->fsm);
  
  /* Source connection handling. */
  if (!transfer_context->source->client)
    transfer_context->source_conn_flags = SSH_FCC_DOWN;

  transfer_context->source_conn_waiting =
    ssh_fsm_condition_create(transfer_context->fsm);

  /* Destination connection handling. */
  if (!transfer_context->destination->client)
    transfer_context->dest_conn_flags = SSH_FCC_DOWN;

  transfer_context->dest_conn_waiting =
    ssh_fsm_condition_create(transfer_context->fsm);

  transfer_context->main_thread =
    ssh_fsm_spawn(fsm, sizeof(*tdata),
                  "fct_next_file_item",
                  NULL /* XXX */, NULL /* XXX */);

  tdata = ssh_fsm_get_tdata(transfer_context->main_thread);
  
  memset(tdata, 0, sizeof(*tdata));
  
  ssh_fsm_set_thread_name(transfer_context->main_thread,
                          "main_thread");

  transfer_context->source_conn_thread =
    ssh_file_copy_connection_thread_spawn(fsm,
                                          transfer_context->
                                          source_conn_waiting,
                                          transfer_context->conn_failed,
                                          transfer_context->source,
                                          &transfer_context->
                                          source_conn_flags);

  transfer_context->dest_conn_thread =
    ssh_file_copy_connection_thread_spawn(fsm,
                                          transfer_context->dest_conn_waiting,
                                          transfer_context->conn_failed,
                                          transfer_context->destination,
                                          &transfer_context->dest_conn_flags);

  ssh_fsm_set_thread_name(transfer_context->source_conn_thread,
                          "source_conn_holder");
  
  ssh_fsm_set_thread_name(transfer_context->dest_conn_thread,
                          "dest_conn_holder");
  
  transfer_context->transfer_timer = ssh_time_measure_allocate();
  transfer_context->total_transfer_timer = ssh_time_measure_allocate();

  transfer_context->operation_handle = 
    ssh_operation_register(transfer_operation_abort_cb, transfer_context);

  return transfer_context->operation_handle;
}
