/*
  sshfc_glob.c

  Author: Sami Lehtinen <sjl@ssh.fi>
          Antti Huima <huima@ssh.fi>

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

  Functions that perform the globbing in file transfer.
 */

#include "sshincludes.h"
#include "sshfilecopy.h"
#include "sshfilecopyi.h"
#include "sshappcommon.h"
#include "sshregex.h"
#include "sshglob.h"

#define SSH_DEBUG_MODULE "SshFCGlob"

/**********
 * ssh_file_copy_glob() related stuff.
 **********/

typedef struct SshFileCopyGlobContextRec
{
  SshFSM fsm;

  /* The attrs, that can be used to affect the globbing behaviour. */
  SshFileCopyGlobAttrs attrs;
  /* Callback that is called when the globbing is ready (whether
     successfully or not), and if the glob has been successful, return
     the list. */
  SshFileCopyGlobReadyCallback ready_cb;
  /* Callback that is callback when a non-fatal error is
     encountered. (can't open some file etc.) */
  SshFileCopyGlobErrorCallback error_cb;
  /* Context, which is passed to the above callabacks.*/
  void *callback_context;
  
  /* Connection used. */
  SshFileCopyConnection connection;
  /* 'Raw' filenames are stored here. */
  SshFileCopyLocation orig_location;
  /* This contains the final file_list. */
  SshDlList new_file_list;

  SshFSMThread main_thread;

} *SshFileCopyGlobContext;

typedef struct GlobThreadContextRec
{
  char *current_basedir;
  
  /* Used in calls to ssh_file_copy_recurse_dirs(), contains
     SshFileCopyFileListItems. */
  SshDlList temp_list;

  /* Current file globbed; this is just a reference pointer, so use
     this as you would use a statically allocated variable.*/
  SshFileCopyFile current_file;

  /* Globbin related data structures. */
  SshRegexContext regex_context;
  SshRegexMatcher pattern_matcher;

} *GlobThreadContext;

void glob_fsm_destructor(void *gdata)
{
  SshFileCopyGlobContext global_data = (SshFileCopyGlobContext) gdata;
  SSH_PRECOND(global_data);

  SSH_DEBUG(3, ("Destroying global data..."));
  ssh_xfree(global_data->attrs);  
}

/* Helper to wrap the ssh_file_copy_file_list_item_destroy()s
   prototype for ssh_app_free_list(). */
void file_item_destroyer(void *item)
{
  SshFileCopyFileListItem to_be_destroyed = (SshFileCopyFileListItem) item;
  SSH_PRECOND(to_be_destroyed);
  
  SSH_DEBUG(3, ("Destroying thread data..."));
  ssh_file_copy_file_list_item_destroy(to_be_destroyed);
}

void glob_thread_destructor(void *tdata)
{
  GlobThreadContext thread_data = (GlobThreadContext)tdata;

  SSH_PRECOND(thread_data);
  
  ssh_xfree(thread_data->current_basedir);
  ssh_app_free_list(thread_data->temp_list, file_item_destroyer);
  if (thread_data->pattern_matcher)
    ssh_regex_free(thread_data->pattern_matcher);
  if (thread_data->regex_context)
    ssh_regex_free_context(thread_data->regex_context);
}

Boolean glob_recurse_file_func(const char *filename,
                               const char *long_name,
                               SshFileAttributes file_attributes,
                               SshFileCopyRecurseAttrs attrs,
                               void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SshFileCopyFileListItem temp_item;
  char *trunc_filename = NULL, *temp_filename;
  SSH_FSM_DATA(SshFileCopyGlobContext, GlobThreadContext);
  
  SSH_DEBUG(5, ("Got file %s.", filename));

  if (strcmp(tdata->current_basedir, filename) == 0)
    return TRUE;
  
  if (strcmp(tdata->current_basedir, "/") == 0)
    trunc_filename = (char *)&filename[1];
  else
    trunc_filename = (char *)&filename[strlen(tdata->current_basedir) + 1];

  SSH_DEBUG(5, ("Truncated filename is %s.", trunc_filename));

  if ((file_attributes->permissions & S_IFMT) == S_IFDIR)
    {      
      if (!(ssh_regex_match_cstr_prefix(tdata->pattern_matcher,
                                        trunc_filename)))
        return FALSE;
      if (ssh_regex_match_cstr(tdata->pattern_matcher, trunc_filename))
        goto full_match;

      return TRUE;
    }
  else
    {
      if (!(ssh_regex_match_cstr(tdata->pattern_matcher, trunc_filename)))
        return FALSE;

      /* Accept this file. */
    full_match:
      temp_item = ssh_file_copy_file_list_item_allocate();
      
      temp_item->original_filename =
        ssh_xstrdup(ssh_file_copy_file_get_name(tdata->current_file));

      if (strlen(tdata->current_basedir) > 0 &&
          strcmp(tdata->current_basedir, "/") != 0)
        {
          ssh_dsprintf(&temp_filename, "%s/%s", tdata->current_basedir,
                       trunc_filename);
        }
      else if (strcmp(tdata->current_basedir, "/") == 0)
        {
          ssh_dsprintf(&temp_filename, "/%s", trunc_filename);
        }
      else
        {
          temp_filename = ssh_xstrdup(trunc_filename);
        }
      
      SSH_TRACE(3, ("Adding file %s to file list...", temp_filename));
      
      ssh_file_copy_location_add_file(temp_item->files,
                                      file_attributes,
                                      temp_filename);

      ssh_xfree(temp_filename);
      
      SSH_VERIFY(ssh_dllist_add_item(gdata->new_file_list,  \
                                     temp_item,             \
                                     SSH_DLLIST_END)        \
                 == SSH_DLLIST_OK);

      return FALSE;      
    }
  SSH_NOTREACHED;
}


void glob_recurse_ready_cb(SshFileCopyError error,
                           const char *error_message,
                           SshFileCopyConnection
                           connection,
                           SshDlList file_list,
                           void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(SshFileCopyGlobContext);

  SSH_PRECOND(connection == gdata->connection);
  SSH_PRECOND(file_list == NULL);
  
  SSH_FSM_SET_NEXT("fcg_next_file");
  
  switch(error)
    {
    case SSH_FC_OK:
      break;
    case SSH_FC_ERROR:
    case SSH_FC_ERROR_DEST_NOT_DIR:
    case SSH_FC_ERROR_NO_SUCH_FILE:
    case SSH_FC_ERROR_PERMISSION_DENIED:
    case SSH_FC_ERROR_FAILURE:
    case SSH_FC_ERROR_ELOOP:
      (*gdata->error_cb)(error, error_message, gdata->callback_context);
      break;
    case SSH_FC_ERROR_CONNECTION_FAILED:
    case SSH_FC_ERROR_CONNECTION_LOST:
    case SSH_FC_ERROR_PROTOCOL_MISMATCH:
      (*gdata->ready_cb)(error, error_message, gdata->connection,
                         NULL, gdata->callback_context);
      SSH_FSM_SET_NEXT("fcg_finish");
      break;
    }

  SSH_FSM_CONTINUE_AFTER_CALLBACK(thread);
}

void glob_recurse_error_cb(SshFileCopyError error,
                           const char *error_message,
                           void *context)
{
  SshFSMThread thread = (SshFSMThread) context;
  SSH_FSM_GDATA(SshFileCopyGlobContext);
  
  (*gdata->error_cb)(error, error_message, gdata->callback_context);
}

SSH_FSM_STEP(glob_next_file)
{
  SshFileCopyFile temp_file;
  SshFileCopyFileListItem temp_item;
  char *temp_filename, *p, *to_be_deleted;
  SSH_FSM_DATA(SshFileCopyGlobContext, GlobThreadContext);

  if (!ssh_dllist_is_current_valid(gdata->orig_location->file_list))
    {
      SSH_DEBUG(3, ("File list is at it's end."));
      SSH_FSM_SET_NEXT("fcg_ready");
      return SSH_FSM_CONTINUE;
    }
      
  temp_file =
    ssh_dllist_current(gdata->orig_location->file_list);

  SSH_ASSERT(temp_file);

  tdata->current_file = temp_file;
  
  ssh_dllist_fw(gdata->orig_location->file_list, 1);

  temp_item = ssh_file_copy_file_list_item_allocate();
      
  temp_item->original_filename =
    ssh_xstrdup(ssh_file_copy_file_get_name(temp_file));

  temp_filename = ssh_xstrdup(temp_item->original_filename);
                                                     
  if ((p = ssh_glob_next_unescaped_wildchar(temp_filename)) != NULL)
    {
      *p = '\0';

      if ((p = strrchr(temp_filename, '/')) != NULL)
        {
          /* Path has more than one component at basepart */
          if (p > &temp_filename[0])
            {
              *p = '\0';
              tdata->current_basedir = ssh_xstrdup(temp_filename);
              ssh_xfree(temp_filename);
              temp_filename =
                ssh_xstrdup(&temp_item->
                            original_filename[strlen
                                             (tdata->current_basedir) + 1]);
            }
          else
            {
              /* The only '/' was at the beginning of the filename. */
              tdata->current_basedir = ssh_xstrdup("/");
              ssh_xfree(temp_filename);
              temp_filename = ssh_xstrdup(&temp_item->original_filename[1]);
            }
        }
      else
        {
          /* No '/' characters at basepart. */
          tdata->current_basedir = ssh_xstrdup("");
          ssh_xfree(temp_filename);
          temp_filename = ssh_xstrdup(temp_item->original_filename);
        }
    }
  else
    {
      /* Doesn't contain wildchars. */
      SSH_TRACE(3, ("Adding file %s to file list without globbing...",  \
                    temp_filename));
      to_be_deleted = temp_filename;
      temp_filename = ssh_glob_strip_escapes(temp_filename);
      ssh_xfree(to_be_deleted);

      /* XXX should be fixed in recurse. */
      if (strlen(temp_filename) < 1)
        {
          ssh_xfree(temp_filename);
          temp_filename = ssh_xstrdup(".");
        }
      
      ssh_file_copy_location_add_file(temp_item->files,
                                      NULL,
                                      temp_filename);

      ssh_xfree(temp_filename);
      
      SSH_VERIFY(ssh_dllist_add_item(gdata->new_file_list,  \
                                     temp_item,             \
                                     SSH_DLLIST_END)        \
                 == SSH_DLLIST_OK);
      return SSH_FSM_CONTINUE;
      
    }
  
  if (tdata->pattern_matcher)
    {
      ssh_regex_free(tdata->pattern_matcher);
    }
  
  tdata->pattern_matcher = ssh_regex_create(tdata->regex_context,
                                            temp_filename,
                                            SSH_REGEX_SYNTAX_ZSH_FILEGLOB);
  
  if (!tdata->pattern_matcher)
    {
      /* XXX FIXME: error message to application. */
      SSH_TRACE(2, ("Pattern ``%s'' was deemed invalid by the " \
                    "sshregex-library.",                        \
                    temp_filename));
      return SSH_FSM_CONTINUE;
    }
  
  ssh_xfree(temp_filename);
  
  /* Strip escaped from basedir before using it. */
  to_be_deleted = tdata->current_basedir;
  tdata->current_basedir = ssh_glob_strip_escapes(tdata->current_basedir);
  ssh_xfree(to_be_deleted);

  /* XXX should be fixed in recurse. */
  if (strlen(tdata->current_basedir) == 0)
    {
      ssh_xfree(tdata->current_basedir);
      tdata->current_basedir = ssh_xstrdup(".");
    }
  
  /* Make a list (that contains only the directory, where we start
     globbing), that can be fed to ssh_file_copy_recurse_dirs(). */
  ssh_file_copy_location_add_file(temp_item->files,
                                  NULL,
                                  tdata->current_basedir);
      
  ssh_file_copy_file_register_filename(temp_item->files->file,
                                       ssh_xstrdup(""));

  if (tdata->temp_list)
    {
      ssh_app_free_list(tdata->temp_list, file_item_destroyer);
    }

  tdata->temp_list = ssh_dllist_allocate();
      
  SSH_VERIFY(ssh_dllist_add_item(tdata->temp_list,  \
                                 temp_item,         \
                                 SSH_DLLIST_END)    \
             == SSH_DLLIST_OK);
      
  SSH_TRACE(2, ("Starting glob of %s...",                   \
                ssh_file_copy_file_get_name(temp_file)));
  SSH_DEBUG(3, ("basedir: %s", tdata->current_basedir));

  
  SSH_FSM_ASYNC_CALL(ssh_file_copy_recurse_dirs \
                     (gdata->connection,        \
                      tdata->temp_list,         \
                      NULL,                     \
                      glob_recurse_file_func,   \
                      glob_recurse_ready_cb,    \
                      glob_recurse_error_cb,    \
                      thread));  
}

SSH_FSM_STEP(glob_ready)
{
  SSH_FSM_GDATA(SshFileCopyGlobContext);

  SSH_FSM_SET_NEXT("fcg_finish");
  
  (*gdata->ready_cb)(SSH_FC_OK, "Globbing successful.", gdata->connection,
                     gdata->new_file_list, gdata->callback_context);
  
  return SSH_FSM_CONTINUE;
}

SSH_FSM_STEP(glob_finish)
{
  SSH_FSM_GDATA(SshFileCopyGlobContext);

  ssh_fsm_destroy(gdata->fsm);
  
  return SSH_FSM_FINISH;
}

SshFSMStateMapItem glob_states_array[] =
{ { "fcg_next_file", "Set next file to glob", glob_next_file },
  { "fcg_ready", "Globbing ready", glob_ready },
  { "fcg_finish", "Finish", glob_finish }
};

void ssh_file_copy_glob(SshFileCopyConnection connection,
                        SshFileCopyLocation orig_location,
                        SshFileCopyGlobAttrs attrs,
                        SshFileCopyGlobReadyCallback ready_cb,
                        SshFileCopyGlobErrorCallback error_cb,
                        void *context)
{
  SshFSM fsm;
  SshFileCopyGlobContext glob_context;
  GlobThreadContext tdata;

  SSH_PRECOND(connection);
  SSH_PRECOND(orig_location);
  SSH_PRECOND(ready_cb);
  SSH_PRECOND(error_cb);
  
  fsm = ssh_fsm_allocate(sizeof(*glob_context),
                         glob_states_array,
                         SSH_FSM_NUM_STATES(glob_states_array),
                         glob_fsm_destructor);

  glob_context = ssh_fsm_get_gdata_fsm(fsm);
  memset(glob_context, 0, sizeof(*glob_context));
  
  glob_context->fsm = fsm;

  if (attrs == NULL)
    {
      attrs = ssh_xcalloc(1, sizeof(*attrs));
    }

  glob_context->connection = connection;
  glob_context->orig_location = orig_location;
  glob_context->attrs = attrs;
  glob_context->ready_cb = ready_cb;
  glob_context->error_cb = error_cb;
  glob_context->callback_context = context;
  glob_context->new_file_list = ssh_dllist_allocate();
  
  ssh_dllist_rewind(orig_location->file_list);

  glob_context->main_thread =
    ssh_fsm_spawn(fsm, sizeof(*tdata),
                  "fcg_next_file",
                  NULL /* XXX */, glob_thread_destructor);

  tdata = ssh_fsm_get_tdata(glob_context->main_thread);
  memset(tdata, 0, sizeof(*tdata));  

  tdata->regex_context = ssh_regex_create_context();

  ssh_fsm_set_thread_name(glob_context->main_thread,
                          "main_thread");
}
