/*
  sshfc_recurse.c

  Author: Sami Lehtinen <sjl@ssh.com>

  Copyright (C) 2000 SSH Communications Security Corp, Helsinki, Finland
  All rights reserved.

  Functions that perform the recursing of subdirectories.
 */

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


#ifdef WIN32
/* There is no symbolic link on Windows */
#define S_IFLNK 0
#endif


#define SSH_DEBUG_MODULE "SshFCRecurse"

/**********
 * ssh_file_copy_recurse_dirs() related stuff.
 **********/

#define SSH_FCR_MAX_SYMLINK_LEVEL_DEPTH 64

/* Global data. */
typedef struct SshFileCopyRecurseContextRec
{
  SshFSM fsm;

  SshFileCopyRecurseErrorCallback error_callback;
  SshFileCopyRecurseReadyCallback completion_callback;  
  void *callback_context;

  SshFSMThread main_thread;

  /* File list given by application. */
  SshDlList file_list;

  /* Optional function to be executed on files, given by
     application. */
  SshFileCopyRecurseFileFunc func;

  /* Recurse attrs, given by application. */
  SshFileCopyRecurseAttrs attrs;

  /* New file list to be returned to application. */
  SshDlList new_file_list;
  
  /* New list item, which is already in the new_file_list, but a
     pointer is kept here for ease of implementation.  */
  SshFileCopyFileListItem new_item;

  /* New directory, which we are currently recursing. */
  SshFileCopyFile new_dir;

  /* Helper variable, used to retain pointer to a dynamically
     malloced string, which is used to lstat a file. The string is
     freed in a callback. */
  char *filename_to_be_statted;
  
  /* Connection handling. */
  SshFileCopyConnection source;
  SshFileCopyLocation location;
  SshFSMThread conn_thread;
  SshFSMCondition conn_waiting;
  SshFSMCondition conn_failed;
  SshUInt8 conn_flags;
  
  /* This is TRUE, if some thread encountered an error, which can't be
     recovered. */
  Boolean fatal_error;
  /* Error message given to application, when fatal error encountered. */
  char *error_message;
      
} *SshFileCopyRecurseContext;

typedef struct RecurseThreadContextRec
{
  /* Parent thread, that will be waken when child is ready. */
  SshFSMThread parent;
  /* Child thread of the thread. */
  SshFSMThread child;

  /* The dir we are currently working on. */
  SshFileCopyFile current_dir;

  /* File, which we got from the last call to readdir. */
  SshFileCopyFile new_file;

  /* The count of symlinks we have traversed (in depth). */
  int symlink_level;
} *RecurseThreadContext;

/* Main thread. */
void recurse_child_thread_destructor(void *tdata)
{
  RecurseThreadContext thread_context = (RecurseThreadContext) tdata;
  
  SSH_PRECOND(thread_context);

#if 0
  if (thread_context->current_dir)
    ssh_file_copy_file_destroy(thread_context->current_dir);
  /* XXX Causes weird errors. Check with atom. */
  if (thread_context->new_file)
    ssh_file_copy_file_destroy(thread_context->new_file);
#endif
}

SSH_FSM_STEP(recurse_next_file)
{
  RecurseThreadContext thread_context;
  char *temp_filename;

  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);
  
  SSH_PRECOND(thread == gdata->main_thread);

  if (gdata->fatal_error)
    {
      /* Return error to application using supplied callback. */
      (*gdata->completion_callback)(SSH_FC_ERROR,
                                    gdata->error_message,
                                    NULL,
                                    NULL,
                                    gdata->callback_context);
      
      ssh_xfree(gdata->error_message);

      /* XXX Kill threads, free memory, etc. */
      SSH_NOTREACHED;
    }
  
  switch (gdata->conn_flags)
    {
    case SSH_FCC_OK:
      break;
    case SSH_FCC_DOWN:
      SSH_TRACE(2, ("Not yet connected, or connection down, waiting..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
      break;
    case SSH_FCC_CONNECTION_FAILED:
      {
        char *error_message;
            
        ssh_dsprintf(&error_message, "Connection to source failed (host = %s, "
                     "user = %s, port = %s).", gdata->source->host,
                     gdata->source->user, gdata->source->port);
            
        /* Return error to application using supplied callback. */
        (*gdata->completion_callback)(SSH_FC_ERROR_CONNECTION_FAILED,
                                      error_message,
                                      NULL,
                                      NULL,
                                      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;
    }
  
 restart:
  if (ssh_dllist_is_current_valid(gdata->file_list))
    {
      SshFileCopyFileListItem item;

      item = (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);

      if (item->files->raw)
        {
          SSH_DEBUG(2, ("File is \"raw\", and it needs to " \
                        "be parsed."));
          SSH_FSM_SET_NEXT("fcr_parse_raw");
          return SSH_FSM_CONTINUE;
        }

      if (!gdata->func && gdata->new_item == NULL)
        {
          gdata->new_item = ssh_file_copy_file_list_item_allocate();
          gdata->new_item->original_filename =
            ssh_xstrdup(item->original_filename);
          gdata->new_item->files->file =
            ssh_file_copy_file_dup(item->files->file);
          gdata->new_item->files->raw = FALSE;
          SSH_VERIFY(ssh_dllist_add_item(gdata->new_file_list,  \
                                         gdata->new_item,       \
                                         SSH_DLLIST_END) ==     \
                     SSH_DLLIST_OK);
      
        }
      
      if (ssh_dllist_is_current_valid(item->files->file_list))
        {
          SshFileCopyFile file;
          
          file =
            (SshFileCopyFile) ssh_dllist_current(item->files->file_list);
          
          /* If basedir is not "" generate filename. */
          if (*(ssh_file_copy_file_get_name(item->files->file)) != '\0')
            {
              const char *current_basedir;

              current_basedir =
                ssh_file_copy_file_get_name(item->files->file);
              
              ssh_dsprintf(&temp_filename, "%s%s%s",
                           current_basedir,
                           current_basedir[strlen(current_basedir) - 1]
                           == '/' ? "" : "/",
                           ssh_file_copy_file_get_name(file));
            }
          else
            {
              temp_filename =
                ssh_xstrdup(ssh_file_copy_file_get_name(file));
            }
          
          if (file->attributes == NULL ||
              !(file->attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS))
            {
              SSH_DEBUG(3, ("File %s needs to be statted.",         \
                            ssh_file_copy_file_get_name(file)));

              gdata->filename_to_be_statted = temp_filename;
              SSH_FSM_SET_NEXT("fcr_lstat");
              return SSH_FSM_CONTINUE;
            }
              
          ssh_dllist_fw(item->files->file_list, 1);
          
          if ((file->attributes->permissions & S_IFMT) == S_IFDIR ||
              ((file->attributes->permissions & S_IFMT) == S_IFLNK &&
               gdata->attrs->follow_symlinks))
            {              
              if (gdata->func &&
                  !(*gdata->func)(temp_filename,
                                  ssh_file_copy_file_get_long_name(file),
                                  ssh_file_copy_file_get_attributes(file),
                                  gdata->attrs,
                                  gdata->callback_context))
                {
                  ssh_xfree(temp_filename);
                  /* This directory was rejected by func for some
                     reason. Skip it. */
                  return SSH_FSM_CONTINUE;
                }

              SSH_DEBUG(3, ("File %s is a directory. Starting "     \
                            "recursion...",                         \
                            ssh_file_copy_file_get_name(file)));

              tdata->child =
                SSH_FSM_FORK(sizeof(*thread_context),
                             "fcr_opendir",
                             NULL,
                             recurse_child_thread_destructor);

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

              thread_context->parent = thread;

              tdata->current_dir = file;

              thread_context->current_dir = ssh_file_copy_file_dup(file);
              ssh_file_copy_file_register_filename(thread_context->current_dir,
                                                   temp_filename);
              if (!gdata->func)
                {
                  gdata->new_dir =
                    ssh_file_copy_file_dup(file);

                  gdata->new_dir->dir_entries =
                    ssh_file_copy_location_allocate(TRUE);

                  gdata->new_dir->dir_entries->parent_dir =
                    gdata->new_item->files;
                  gdata->new_dir->dir_entries->file = gdata->new_dir;

                  SSH_VERIFY(ssh_dllist_add_item                        \
                             (gdata->new_item->files->file_list,        \
                              gdata->new_dir,                           \
                              SSH_DLLIST_END) == SSH_DLLIST_OK);

                  thread_context->current_dir->dir_entries =
                    gdata->new_dir->dir_entries;
                }

              return SSH_FSM_SUSPENDED;
            }
          else
            {
              SSH_DEBUG(3, ("File %s is not a directory. Adding it " \
                            "to list...",                            \
                            ssh_file_copy_file_get_name(file)));

              if (gdata->func)
                {
                  (void)(*gdata->func)(temp_filename,
                                       ssh_file_copy_file_get_long_name(file),
                                       ssh_file_copy_file_get_attributes(file),
                                       gdata->attrs,
                                       gdata->callback_context);
                }
              else
                {
                  /* Add to list. */
                  SSH_VERIFY(ssh_dllist_add_item                    \
                             (gdata->new_item->files->file_list,    \
                              ssh_file_copy_file_dup(file),         \
                              SSH_DLLIST_END) == SSH_DLLIST_OK);
                }
              
              ssh_xfree(temp_filename);
              return SSH_FSM_CONTINUE;
            }
        }
      else
        {
          if (ssh_dllist_length(item->files->file_list) < 1)
            {
              /* XXX When the basedir is the only file, we should still
                 recurse through it. */
              temp_filename = ssh_xstrdup
                (ssh_file_copy_file_get_name(item->files->file));
              
#if 0
              if (file->attributes == NULL ||
                  !(file->attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS))
                {
                  SSH_DEBUG(3, ("File %s needs to be statted.",         \
                                ssh_file_copy_file_get_name(file)));
                  
                  gdata->filename_to_be_statted = temp_filename;
                  SSH_FSM_SET_NEXT("fcr_lstat");
                  return SSH_FSM_CONTINUE;
                }
#endif         
              /* XXX This needs to be fixed. */
              SSH_ASSERT(item->files->file->attributes);
              SSH_ASSERT(item->files->file->attributes->flags & \
                         SSH_FILEXFER_ATTR_PERMISSIONS);              

              if ((item->files->file->attributes->permissions & S_IFMT) ==
                  S_IFDIR ||
                  ((item->files->file->attributes->permissions & S_IFMT) ==
                  S_IFLNK && gdata->attrs->follow_symlinks))
                {              
                  if (gdata->func &&
                      !(*gdata->func)(temp_filename,
                                      ssh_file_copy_file_get_long_name
                                      (item->files->file),
                                      ssh_file_copy_file_get_attributes
                                      (item->files->file),
                                      gdata->attrs,
                                      gdata->callback_context))
                    {
                      ssh_xfree(temp_filename);
                      /* This directory was rejected by func for some
                         reason. Skip it. */
                      return SSH_FSM_CONTINUE;
                    }
                  SSH_DEBUG(3, ("File %s is a directory. Starting "     \
                                "recursion...",                         \
                                ssh_file_copy_file_get_name             \
                                (item->files->file)));

                  tdata->child =
                    SSH_FSM_FORK(sizeof(*thread_context),
                                 "fcr_opendir",
                                 NULL,
                                 recurse_child_thread_destructor);

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

                  thread_context->parent = thread;

                  tdata->current_dir = item->files->file;

                  thread_context->current_dir =
                    ssh_file_copy_file_dup(item->files->file);
                  ssh_file_copy_file_register_filename
                    (thread_context->current_dir,
                     temp_filename);

                  if (!gdata->func)
                    {
                      gdata->new_dir =
                        ssh_file_copy_file_dup(tdata->current_dir);

                      gdata->new_dir->dir_entries =
                        gdata->new_item->files;

                      gdata->new_dir->dir_entries->parent_dir =
                        NULL;

                      thread_context->current_dir->dir_entries =
                        gdata->new_dir->dir_entries;
                    }
                }
              else
                {
                  /* XXX Error handling here. */
                  SSH_NOTREACHED;
                }
              
              ssh_dllist_fw(gdata->file_list, 1);
              return SSH_FSM_SUSPENDED;
            }

          SSH_DEBUG(4, ("Current location list is at it's end. Moving " \
                        "to next..."));
          ssh_dllist_fw(gdata->file_list, 1);
          gdata->new_item = NULL;
          goto restart;
        }
    }
  else
    {
      SSH_DEBUG(3, ("No more files in this list."));
      SSH_FSM_SET_NEXT("fcr_recurse_done");
      return SSH_FSM_CONTINUE;
    }
  
  return SSH_FSM_SUSPENDED;
}

void recurse_parse_stat_cb(SshFileClientError error,
                           SshFileAttributes attributes,
                           void *context)
{
  SshDlListNode node;
  SshFileCopyRecurseContext gdata;
  SshFileCopyFileListItem item;
  SshFileCopyFile file;
  SshFileCopyLocation to_be_deleted;
  SshFSMThread thread;
  SshFileCopyError error_code;
  char *error_string;
  
  thread = (SshFSMThread) context;
  
  gdata = (SshFileCopyRecurseContext) ssh_fsm_get_gdata(thread);  

  item = (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);

  SSH_ASSERT(item);
  SSH_ASSERT(item->files->raw);
  
  SSH_DEBUG(4, ("Received error %d.", error));

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

  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. */
      to_be_deleted = item->files;

      item->files = ssh_file_copy_location_allocate(TRUE);

      ssh_file_copy_location_add_file(item->files,
                                      attributes,
                                      ssh_file_copy_file_get_name(file));

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

    case SSH_FX_OP_UNSUPPORTED:
      /* This op should most definitely be supported. */
      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:
      node = ssh_dllist_current_node(gdata->file_list);
      ssh_dllist_delete_node(node);
      ssh_file_copy_file_list_item_destroy(item);

      /* 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("fcr_parse_raw");
      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..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_SET_NEXT("fcr_parse_raw");
      gdata->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.",
                                    NULL,
                                    NULL,
                                    gdata->callback_context);

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

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(recurse_parse_raw)
{
  SshFileCopyFileListItem item;
  SshFileCopyFile file;

  SSH_FSM_GDATA(SshFileCopyRecurseContext);
  
  if (gdata->conn_flags == SSH_FCC_DOWN)
    {
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
    }
  
  SSH_FSM_SET_NEXT("fcr_next_file");
  
  item = (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);

  SSH_ASSERT(item);
  SSH_ASSERT(item->files->raw);
  SSH_ASSERT(item->files->source);
  
  /* stat file */  
  SSH_ASSERT(ssh_dllist_is_current_valid(item->files->file_list));
  file = (SshFileCopyFile) ssh_dllist_current(item->files->file_list);

  SSH_ASSERT(file);

  SSH_FSM_ASYNC_CALL(ssh_file_client_stat(gdata->source->client,             \
                                          ssh_file_copy_file_get_name(file), \
                                          recurse_parse_stat_cb, thread));  
}

#if 0
  ssh_dsprintf(&gdata->error_message,
                   "Encountered a fatal error when recursing "
                   "file (dir) %s%s%s%s%s%s%s .",
                   gdata->source->user ? gdata->source->user : "",
                   gdata->source->user ? "@" : "",
                   gdata->source->host ? gdata->source->host : "",
                   gdata->source->port ? "" : "#",
                   gdata->source->port ? gdata->source->port : "",
                   gdata->source->host ? ":" : "",
                   xxx);
#endif 

void recurse_lstat_cb(SshFileClientError error,
                      SshFileAttributes attributes,
                      void *context)
{
  SshFileCopyRecurseContext gdata;
  SshFileCopyError error_code;
  char *error_string;
  SshFileCopyFileListItem item;
  SshFileCopyFile file;
  SshFSMThread thread = (SshFSMThread) context;
  
  gdata = ssh_fsm_get_gdata(thread);

  ssh_xfree(gdata->filename_to_be_statted);
  gdata->filename_to_be_statted = NULL;
  
  SSH_ASSERT(ssh_dllist_is_current_valid(gdata->file_list));
  item = (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);
  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:
      /* XXX */
      ssh_file_copy_file_register_attributes
        (file, ssh_file_attributes_dup(attributes));
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

    case SSH_FX_OP_UNSUPPORTED:
      /* This op should most definitely be supported. */
      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:
      /* Skip file. */
      ssh_dllist_fw(item->files->file_list, 1);

      /* 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("fcr_lstat_file");
      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..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      gdata->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.",
                                    NULL,
                                    NULL,
                                    gdata->callback_context);

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

SSH_FSM_STEP(recurse_lstat_file)
{
  SshFileCopyFileListItem item;
  SshFileCopyFile file;
  
  SSH_FSM_GDATA(SshFileCopyRecurseContext);

  if (gdata->conn_flags == SSH_FCC_DOWN)
    {
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
    }

  SSH_FSM_SET_NEXT("fcr_next_file");

  SSH_ASSERT(ssh_dllist_is_current_valid(gdata->file_list));
  
  item = (SshFileCopyFileListItem) ssh_dllist_current(gdata->file_list);

  SSH_ASSERT(ssh_dllist_is_current_valid(item->files->file_list));

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

  SSH_FSM_ASYNC_CALL(ssh_file_client_lstat(gdata->source->client,
                                           gdata->filename_to_be_statted,
                                           recurse_lstat_cb,
                                           thread));  
}

void recurse_opendir_cb(SshFileClientError error,
                        SshFileHandle handle,
                        void *context)
{
  SshFileCopyError error_code;
  char *error_string;
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);

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

  switch (error)
    {
    case SSH_FX_OK:
      /* XXX */
      tdata->current_dir->handle = handle;
      break;
    case SSH_FX_EOF:
      /* This should not happen, ever. */
      SSH_NOTREACHED;

    case SSH_FX_OP_UNSUPPORTED:
      /* This op should most definitely be supported. */
      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_dir));
      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_dir));
      goto error_common;      
    case SSH_FX_FAILURE:  
      if ((tdata->current_dir->attributes->permissions & S_IFMT) == S_IFLNK &&
          gdata->attrs->follow_symlinks)
        {
          /* We tried to follow a symlink, which we assumed to be
             a directory.*/
          SSH_TRACE(2, ("file '%s', a symlink, apparently wasn't a "            \
                        "directory. Tata!",                                     \
                        ssh_file_copy_file_get_name(tdata->current_dir)));
          SSH_FSM_SET_NEXT("fcr_wake_parent_and_finish");
          break;
        }
      /* 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(tdata->current_dir));
    error_common:
      /* As opendir is always called in a new child, we just report
         the error as non-fatal to the application, and wake up the
         parent. */
      SSH_FSM_SET_NEXT("fcr_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:  
      /* XXX These cases aren't handled this easily. If connection is
         lost in the middle of the recursion, ALL filehandles become
         invalid. What this means, that if we are already recursing
         some subdir, and not just opening the first dir, we must
         start the recursion from the beginning, as all readdir-calls
         etc. etc. would be directed to invalid filehandles. Here, we
         currently assert if we are directly under the main
         thread. This does not take the problem away, but we won't go
         in to a situation where the error would be VERY obscure. */

      SSH_VERIFY(tdata->parent == gdata->main_thread);
      
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      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..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      gdata->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.",
                                    NULL,
                                    NULL,
                                    gdata->callback_context);

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

  SSH_DEBUG_UNINDENT;
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(recurse_opendir)
{
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);

  SSH_PRECOND(tdata->current_dir);
  SSH_PRECOND(tdata->parent);
  SSH_PRECOND(tdata->current_dir->attributes);
  SSH_PRECOND(tdata->current_dir->attributes->flags &   \
              SSH_FILEXFER_ATTR_PERMISSIONS);
  SSH_PRECOND((tdata->current_dir->attributes->permissions & S_IFMT) ==         \
              S_IFDIR ||                                                        \
              (gdata->attrs->follow_symlinks ||                                 \
               (tdata->current_dir->attributes->permissions & S_IFMT) ==        \
               S_IFLNK));

  if (gdata->conn_flags == SSH_FCC_DOWN)
    {
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
    }

  if (tdata->symlink_level > SSH_FCR_MAX_SYMLINK_LEVEL_DEPTH)
    {
      char *error_string;
      
      ssh_dsprintf(&error_string, "%s: Max symlink count for depth exceeded "
                   "(we have very probably encountered a symlink loop. See "
                   "man-page for scp2).",
                   ssh_file_copy_file_get_name(tdata->current_dir));
      /* As opendir is always called in a new child, we just report
         the error as non-fatal to the application, and wake up the
         parent. */
      SSH_FSM_SET_NEXT("fcr_wake_parent_and_finish");
      /* Report error. */
      (*gdata->error_callback)(SSH_FC_ERROR_ELOOP, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      return SSH_FSM_CONTINUE;
    }

  if (gdata->attrs->follow_symlinks ||
      (tdata->current_dir->attributes->permissions & S_IFMT) == S_IFLNK)
    {
      tdata->symlink_level++;
      SSH_DEBUG(4, ("symlink_level: %d", tdata->symlink_level));
    }

  SSH_DEBUG_INDENT;

  SSH_TRACE(3, ("Opening directory %s...",                              \
                ssh_file_copy_file_get_name(tdata->current_dir)));
  
  SSH_FSM_SET_NEXT("fcr_readdir");
  
  SSH_FSM_ASYNC_CALL(ssh_file_client_opendir                            \
                     (gdata->source->client,                            \
                      ssh_file_copy_file_get_name(tdata->current_dir),  \
                      recurse_opendir_cb,                               \
                      thread));
}

void recurse_readdir_cb(SshFileClientError error,
                        const char *name,
                        const char *long_name,
                        SshFileAttributes attrs,
                        void *context)
{
  SshFileCopyError error_code;
  char *error_string;
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);
  
  SSH_DEBUG(4, ("Received error %d.", error));

  switch (error)
    {
    case SSH_FX_OK:
      /* Everything went ok, register new file, and move to next step. */

      /* Skip "." and ".." */
      if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0)
        {
          SSH_FSM_SET_NEXT("fcr_readdir");
          break;
        }      
      
      tdata->new_file = ssh_file_copy_file_allocate();
      ssh_file_copy_file_register_filename(tdata->new_file,
                                           ssh_xstrdup(name));
      ssh_file_copy_file_register_long_name(tdata->new_file,
                                            ssh_xstrdup(long_name));
      ssh_file_copy_file_register_attributes(tdata->new_file,
                                             ssh_file_attributes_dup(attrs));
      break;
    case SSH_FX_EOF:
      /* We are at the end of the directory. Add list we gathered to XXX.
         Wake up parent and finish. */
      SSH_FSM_SET_NEXT("fcr_current_dir_at_end");
      break;
      /* 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_dir));
      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_dir));
      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(tdata->current_dir));
    error_common:
      /* XXX */
      SSH_FSM_SET_NEXT("fcr_readdir");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:  
      /* XXX These cases aren't handled this easily. If connection is
         lost in the middle of the recursion, ALL filehandles become
         invalid. What this means, that if we are already recursing
         some subdir, and not just opening the first dir, we must
         start the recursion from the beginning, as all readdir-calls
         etc. etc. would be directed to invalid filehandles. Here, we
         currently assert if we are directly under the main
         thread. This does not take the problem away, but we won't go
         in to a situation where the error would be VERY obscure. */

      SSH_VERIFY(tdata->parent == gdata->main_thread);
      
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      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..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      gdata->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.",
                                    NULL,
                                    NULL,
                                    gdata->callback_context);

      /* XXX Free everything, kill threads. */
      SSH_NOTREACHED;
      return;
    case SSH_FX_OP_UNSUPPORTED:
      /* This op should most definitely be supported. */
      SSH_NOTREACHED;

    }  
  
  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

SSH_FSM_STEP(recurse_readdir)
{
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);

  SSH_PRECOND(tdata->current_dir);
  SSH_PRECOND(tdata->current_dir->handle);
  
  SSH_PRECOND(tdata->parent);

  if (gdata->conn_flags == SSH_FCC_DOWN)
    {
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
    }

  SSH_FSM_SET_NEXT("fcr_process_file");
  
  SSH_FSM_ASYNC_CALL(ssh_file_client_readdir            \
                     (tdata->current_dir->handle,       \
                      recurse_readdir_cb,               \
                      thread));
}

SSH_FSM_STEP(recurse_process_file)
{
  Boolean func_ret = FALSE;
  char *temp_filename;  
  RecurseThreadContext thread_context;
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);
  
  if (!(tdata->new_file->attributes->flags & SSH_FILEXFER_ATTR_PERMISSIONS))
    {
      SSH_FSM_SET_NEXT("fcr_readdir_lstat_file");
      return SSH_FSM_CONTINUE;
    }

  SSH_FSM_SET_NEXT("fcr_readdir");
  
  if (*(ssh_file_copy_file_get_name(tdata->current_dir)) != '\0')
    {
      const char *current_basedir;
      
      current_basedir =
        ssh_file_copy_file_get_name(tdata->current_dir);
      
      ssh_dsprintf(&temp_filename, "%s%s%s",
                   current_basedir,
                   current_basedir[strlen(current_basedir) - 1]
                   == '/' ? "" : "/",
                   ssh_file_copy_file_get_name(tdata->new_file));
    }
  else
    {
      temp_filename =
        ssh_xstrdup(ssh_file_copy_file_get_name(tdata->new_file));
    }
  
  if (gdata->func)
    {
      func_ret =
        (*gdata->func)(temp_filename,
                       ssh_file_copy_file_get_long_name(tdata->new_file),
                       ssh_file_copy_file_get_attributes(tdata->new_file),
                       gdata->attrs,
                       gdata->callback_context);
    }
  else
    {
      SSH_VERIFY(ssh_dllist_add_item                                    \
                 (tdata->current_dir->dir_entries->file_list,           \
                  tdata->new_file, SSH_DLLIST_END) == SSH_DLLIST_OK);
    }

  if ((tdata->new_file->attributes->permissions & S_IFMT) == S_IFDIR ||
      ((tdata->new_file->attributes->permissions & S_IFMT) == S_IFLNK &&
       gdata->attrs->follow_symlinks))
    {
      if (!gdata->func)
        {
          tdata->new_file->dir_entries = ssh_file_copy_location_allocate(TRUE);
          tdata->new_file->dir_entries->parent_dir =
            tdata->current_dir->dir_entries;
          tdata->new_file->dir_entries->file = tdata->new_file;
        }
      else if (func_ret == FALSE)
        {
          SSH_DEBUG(3, ("SshFileRecurseFunc returned FALSE, so we "     \
                        "don't want to traverse this directory."));
          ssh_xfree(temp_filename);
          ssh_file_copy_file_destroy(tdata->new_file);
          return SSH_FSM_CONTINUE;
        }
      
      tdata->child = SSH_FSM_FORK(sizeof(*thread_context),
                                  "fcr_opendir",
                                  NULL,
                                  recurse_child_thread_destructor);

      thread_context = ssh_fsm_get_tdata(tdata->child);

      memset(thread_context, 0, sizeof(*thread_context));

      thread_context->parent = thread;
      
      thread_context->current_dir = ssh_file_copy_file_dup(tdata->new_file);
      thread_context->symlink_level = tdata->symlink_level;
      
      ssh_file_copy_file_register_filename(thread_context->current_dir,
                                           temp_filename);      

      return SSH_FSM_SUSPENDED;
    }
  else
    {
      ssh_xfree(temp_filename);
    }  

  return SSH_FSM_CONTINUE;
}

void recurse_readdir_lstat_cb(SshFileClientError error,
                              SshFileAttributes attributes,
                              void *context)
{
  SshFileCopyError error_code;
  char *error_string;
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);
  
  ssh_xfree(gdata->filename_to_be_statted);
  gdata->filename_to_be_statted = NULL;
  
  SSH_DEBUG(4, ("Received error %d.", error));

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

    case SSH_FX_OP_UNSUPPORTED:
      /* This op should most definitely be supported. */
      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->new_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->new_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(tdata->new_file));
    error_common:
      /* Skip file. */
      /* XXX We should probably free something here. */
      SSH_FSM_SET_NEXT("fcr_readdir");
      /* Report error. */
      (*gdata->error_callback)(error_code, error_string,
                               gdata->callback_context);
      ssh_xfree(error_string);
      break;

    case SSH_FX_NO_CONNECTION:  
      /* XXX These cases aren't handled this easily. If connection is
         lost in the middle of the recursion, ALL filehandles become
         invalid. What this means, that if we are already recursing
         some subdir, and not just opening the first dir, we must
         start the recursion from the beginning, as all readdir-calls
         etc. etc. would be directed to invalid filehandles. */

      SSH_NOTREACHED;
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      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..."));
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_SET_NEXT("fcr_lstat_file");
      gdata->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.",
                                    NULL,
                                    NULL,
                                    gdata->callback_context);

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

}

SSH_FSM_STEP(recurse_readdir_lstat_file)
{
  SSH_FSM_DATA(SshFileCopyRecurseContext, RecurseThreadContext);
  
  if (gdata->conn_flags == SSH_FCC_DOWN)
    {
      SSH_FSM_THROW(gdata->conn_thread, SSH_FCC_CONN_DOWN);
      SSH_FSM_CONDITION_WAIT(gdata->conn_waiting);
    }

  SSH_FSM_SET_NEXT("fcr_process_file");

  if (*(ssh_file_copy_file_get_name(tdata->current_dir)) != '\0')
    {
      const char *current_basedir;
      
      current_basedir =
        ssh_file_copy_file_get_name(tdata->current_dir);
      
      ssh_dsprintf(&gdata->filename_to_be_statted, "%s%s%s",
                   current_basedir,
                   current_basedir[strlen(current_basedir) - 1]
                   == '/' ? "" : "/",
                   ssh_file_copy_file_get_name(tdata->new_file)); 
    }
  else
    {    
      gdata->filename_to_be_statted =
        ssh_xstrdup(ssh_file_copy_file_get_name(tdata->new_file));
    }
  
  SSH_FSM_ASYNC_CALL(ssh_file_client_lstat(gdata->source->client,
                                           gdata->filename_to_be_statted,
                                           recurse_readdir_lstat_cb,
                                           thread));
}

SSH_FSM_STEP(recurse_current_dir_at_end)
{
  SSH_FSM_TDATA(RecurseThreadContext);
  ssh_file_client_close(tdata->current_dir->handle, NULL, NULL);
  tdata->current_dir->handle = NULL;
  SSH_FSM_SET_NEXT("fcr_wake_parent_and_finish");
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(recurse_wake_parent_and_finish)
{
  SSH_FSM_TDATA(RecurseThreadContext);

  if (tdata->parent)
    ssh_fsm_continue(tdata->parent);
  
  return SSH_FSM_FINISH;
}

SSH_FSM_STEP(recurse_done)
{
  SSH_FSM_GDATA(SshFileCopyRecurseContext);

  (*gdata->completion_callback)(SSH_FC_OK,
                                "Recursion complete.",
                                gdata->source,
                                gdata->new_file_list,
                                gdata->callback_context);
  
  ssh_fsm_kill_thread(gdata->conn_thread);
  ssh_fsm_destroy(gdata->fsm);
  
  return SSH_FSM_FINISH;
}

SshFSMStateMapItem recurse_states_array[] =
{ { "fcr_next_file", "Set next file", recurse_next_file },
  { "fcr_lstat", "lstat file", recurse_lstat_file },
  { "fcr_parse_raw", "Parse \"raw\" file", recurse_parse_raw },
  { "fcr_recurse_done", "We're done recursing", recurse_done },
  
  /* Connection handling. */
#include "sshfc_conn_states.h"

  /* Child threads check directories "recursively". */
  { "fcr_opendir", "Open a directory", recurse_opendir },
  { "fcr_readdir", "Take next directory element", recurse_readdir },
  { "fcr_process_file", "Process new file", recurse_process_file },
  { "fcr_readdir_lstat_file", "lstat a file if it doesn't have permissions",
    recurse_readdir_lstat_file },
  { "fcr_current_dir_at_end", "We are finished processing the current dir",
    recurse_current_dir_at_end },
  { "fcr_wake_parent_and_finish", "Wake parent and finish",
    recurse_wake_parent_and_finish }
};

void recurse_fsm_destructor(void *gdata)
{
  SSH_PRECOND(gdata);
  /* XXX */
}

void ssh_file_copy_recurse_dirs(SshFileCopyConnection source,
                                SshDlList file_list,
                                SshFileCopyRecurseAttrs attrs,
                                SshFileCopyRecurseFileFunc func,
                                SshFileCopyRecurseReadyCallback ready_cb,
                                SshFileCopyRecurseErrorCallback error_cb,
                                void *context)
{
  SshFileCopyRecurseContext recurse_context;
  RecurseThreadContext tdata;
  
  SshFSM fsm;
  
  SSH_PRECOND(source);
  SSH_PRECOND(file_list);
  SSH_PRECOND(ready_cb);
  SSH_PRECOND(error_cb);

  fsm = ssh_fsm_allocate(sizeof(*recurse_context),
                         recurse_states_array,
                         SSH_FSM_NUM_STATES(recurse_states_array),
                         recurse_fsm_destructor);

  recurse_context = ssh_fsm_get_gdata_fsm(fsm);

  memset(recurse_context, 0, sizeof(*recurse_context));

  recurse_context->fsm = fsm;
  recurse_context->completion_callback = ready_cb;
  recurse_context->error_callback = error_cb;
  recurse_context->callback_context = context;
  recurse_context->source = source;
  recurse_context->func = func;

  if (!attrs)
    attrs = ssh_xcalloc(1, sizeof(*attrs));
  
  recurse_context->attrs = attrs;
  
  ssh_dllist_rewind(file_list);
  ssh_dllist_mapcar(file_list, location_list_rewind, NULL);

  recurse_context->conn_failed =
    ssh_fsm_condition_create(recurse_context->fsm);

  recurse_context->file_list = file_list;

  if (!recurse_context->func)
    recurse_context->new_file_list = ssh_dllist_allocate();
  
  /* Connection handling. */
  if (!recurse_context->source->client)
    recurse_context->conn_flags = SSH_FCC_DOWN;

  recurse_context->conn_waiting =
    ssh_fsm_condition_create(recurse_context->fsm);

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

  ssh_fsm_set_thread_name(recurse_context->main_thread,
                          "main_thread");
  
  tdata = ssh_fsm_get_tdata(recurse_context->main_thread);
  
  memset(tdata, 0, sizeof(*tdata));
  
  recurse_context->conn_thread =
    ssh_file_copy_connection_thread_spawn(fsm,
                                          recurse_context->
                                          conn_waiting,
                                          recurse_context->conn_failed,
                                          recurse_context->source,
                                          &recurse_context->
                                          conn_flags);


  ssh_fsm_set_thread_name(recurse_context->conn_thread,
                          "conn_holder");
  
}
