/*

  sshsftpcwd.c

  Author: Tomi Salo <ttsalo@ssh.com>

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

  Created Wed Jan  5 21:52:44 2000.

  Auxiliary stuff implementing CWD in sftp2. (When using
  sshfilecopy).
  
  */

#include "sshincludes.h"
#include "sftpcwd.h"
#include "sshdllist.h"
#include "sshfilexfer.h"
#include "sshtimeouts.h"

#define SSH_DEBUG_MODULE "SftpCwd"

/* A global buffer for constructing paths before they are ssh_xstrdupped.
   The stored path must not exceed MAXPATHLEN. The strings supplied
   by the user to _add and _strip -functions must also not exceed the
   MAXPATHLEN. These two conditions will prevent buffer overruns.
   XXX CHECK THESE THINGS */
unsigned char tmp_string[2*MAXPATHLEN];

void
ssh_sftp_cwd_timeout_cb(void *context);

struct SshSftpCwdContextRec
{
  SshDlList path;
  SshFileClient file_client;
  void *user_context;
  SshSftpChangeCwdCb change_cwd_cb;
  unsigned char *change_cwd_path; /* New path (a temporary storage) */
  unsigned char *path_to_be_checked;
};

/* Initialize the cwd stuff and get your very own context! */
SshSftpCwdContext
ssh_sftp_cwd_init(SshFileClient file_client)
{
  SshSftpCwdContext ctx;

  ctx = ssh_xcalloc(1, sizeof(*ctx));
  ctx->path = ssh_dllist_allocate();
  ctx->file_client = file_client;

  return ctx;
}

/* Destroy the context. Do not use it after this. */
void
ssh_sftp_cwd_uninit(SshSftpCwdContext cwd_context)
{
  unsigned char *tmp_ptr;
  
  /* Clean the list, free the strings. */
  ssh_dllist_rewind(cwd_context->path);
  while ((tmp_ptr = ssh_dllist_delete_current(cwd_context->path)) != NULL)
    ssh_xfree(tmp_ptr);

  ssh_dllist_free(cwd_context->path);
  ssh_xfree(cwd_context);
}

/* Adds the cwd in front of the given string.
   Returns an xmallocated string, or NULL in case something
   went wrong. 'path' is permitted to be NULL.*/
unsigned char *
ssh_sftp_cwd_add(const unsigned char *path,
                 SshSftpCwdContext cwd_context)
{
  unsigned char *tmp_string_ptr, *path_elem;
  if (path != NULL && strlen(path) >= MAXPATHLEN)
    return NULL;
  tmp_string_ptr = tmp_string;
  ssh_dllist_rewind(cwd_context->path);
  /* Insert all the path elements into the tmp_string */
  while ((path_elem = ssh_dllist_current(cwd_context->path)) != NULL)
    {
      strcpy(tmp_string_ptr, path_elem);
      tmp_string_ptr += strlen(path_elem);
      if (strcmp(path_elem, "/"))
        *tmp_string_ptr++ = '/';
      ssh_dllist_fw(cwd_context->path, 1);
    }
  if (path != NULL)
    strcpy(tmp_string_ptr, path);
  else
    tmp_string_ptr[0] = 0;
  SSH_DEBUG(7, ("Complete path: %s", tmp_string));
  return ssh_xstrdup(tmp_string);
}

/* Strips the cwd from the front of the given path.
   Returns an xmallocated string or NULL if the given
   path did not contain the cwd. */
unsigned char *
ssh_sftp_cwd_strip(const unsigned char *path,
                   SshSftpCwdContext cwd_context,
                   Boolean strip_dot_prefix)
{
  unsigned char *tmp_string_ptr, *path_elem;

  if (strlen(path) >= MAXPATHLEN)
    return NULL;
  tmp_string_ptr = tmp_string;

  ssh_dllist_rewind(cwd_context->path);
  /* Insert all the path elements into the tmp_string */
  while ((path_elem = ssh_dllist_current(cwd_context->path)) != NULL)
    {
      strcpy(tmp_string_ptr, path_elem);
      tmp_string_ptr += strlen(path_elem);
      if (strcmp(path_elem, "/"))
        *tmp_string_ptr++ = '/';
      ssh_dllist_fw(cwd_context->path, 1);
    }

  /* Terminate the path... */
  *tmp_string_ptr = 0;
  
  /* Check the user-supplied path */
  if (!strncmp(tmp_string, path, strlen(tmp_string)))
    {
      /* The user-supplied path does begin with the cwd. */
      /* ./foo -> foo if strip_dot_prefix is set. */
      if (strip_dot_prefix == TRUE &&
          !strncmp(path + strlen(tmp_string), "./", 2))
        return ssh_xstrdup(path + strlen(tmp_string) + 2);
      else
        return ssh_xstrdup(path + strlen(tmp_string));
    }
  else
    {
      /* User-supplied path does not begin with the cwd. */
      return NULL;
    }
}

/* Strip a directory/.. sequence in the list, if one exists.
   TRUE: sequence was stripped. FALSE: no such sequence was found. */
Boolean
ssh_sftp_cwd_strip_dotdots(SshDlList path)
{
  unsigned char *path_elem;
  
  ssh_dllist_rewind(path);
  while ((path_elem = ssh_dllist_current(path)) != NULL)
    {
      if (strcmp(path_elem, "..") &&
          ssh_dllist_node_get_next(ssh_dllist_current_node(path)) != NULL &&
          ssh_dllist_node_get_item(ssh_dllist_node_get_next(ssh_dllist_current_node(path))) != NULL &&
          !strcmp(ssh_dllist_node_get_item(ssh_dllist_node_get_next(ssh_dllist_current_node(path))), ".."))
        {
          /* Found a dir/.. sequence. Delete the two nodes. */
          ssh_xfree(ssh_dllist_delete_current(path));
          ssh_xfree(ssh_dllist_delete_current(path));
          return TRUE;
        }
      ssh_dllist_fw(path, 1);
    }
  return FALSE;
}

void
ssh_sftp_cwd_change_cb(SshFileClientError error,
                       SshFileAttributes attributes,
                       void *context)
{
  SshSftpCwdContext cwd_ctx = (SshSftpCwdContext)context;
  unsigned char *path, *tmp_ptr, *prev_ptr;
  Boolean exit_condition = FALSE;

  if (error == SSH_FX_NO_CONNECTION)
    {
      /* Let's schedule a timeout and try again. */
      ssh_register_timeout(1L, 0L, ssh_sftp_cwd_timeout_cb, context);
      return;
    }
  
  ssh_xfree(cwd_ctx->path_to_be_checked);
      
  /* If the proposed path exists, we'll replace the old
     path stored in the list with it. */
  if (error == SSH_FX_OK)
    {
      path = cwd_ctx->change_cwd_path;
      SSH_DEBUG(7, ("Path OK: %s", path));

      /* Delete the old path-list contents. */
      ssh_dllist_rewind(cwd_ctx->path);
      while ((tmp_ptr = ssh_dllist_delete_current(cwd_ctx->path)) != NULL)
        ssh_xfree(tmp_ptr);

      /* Handle the special case ('/' not a path element separator) */
      if (path[0] == '/')
        {
          ssh_dllist_add_item(cwd_ctx->path, ssh_xstrdup("/"),
                              SSH_DLLIST_END);
          path++;
        }

      /* Split the remaining path elements into the list. */
      while (exit_condition == FALSE)
        {
          prev_ptr = path;
          while (path[0] != '/' && path[0] != 0)
            path++;
          if (path[0] == 0)
            exit_condition = TRUE;
          *path++ = 0;
          if (strlen(prev_ptr) != 0)
            {
              SSH_DEBUG(10, ("Adding path element: %s", prev_ptr));
              ssh_dllist_add_item(cwd_ctx->path, ssh_xstrdup(prev_ptr),
                                  SSH_DLLIST_END);
            }
        }

      while (ssh_sftp_cwd_strip_dotdots(cwd_ctx->path) == TRUE)
        ;

      cwd_ctx->change_cwd_cb(SSH_SFTP_CWD_OK, cwd_ctx->user_context);
    }
  else
    {
      cwd_ctx->change_cwd_cb(SSH_SFTP_CWD_ERROR, cwd_ctx->user_context);
    }
  /* This was dupped previously. */
  ssh_xfree(cwd_ctx->change_cwd_path);
  cwd_ctx->change_cwd_path = NULL;
}

/* Tries to change the cwd. Calls the callback when ready. */
void
ssh_sftp_cwd_change(unsigned char *path,
                    SshSftpCwdContext cwd_ctx,
                    SshSftpChangeCwdCb cwd_cb,
                    void *context)
{
  /* Case 1: We're given an absolute path. If it exists,
     it will replace the old path completely.
     Case 2: We're given a relative path. We'll have to
     concatenate it with the old cwd, check it's existence
     and then either keep the old cwd or replace it with
     the new, concatenated path.

     Case 1: Just check the path with sshfilexfer.
     If it exists, split it into the list and do
     post-processing.
     Case 2: Form a whole path using ssh_sftp_add_cwd
     and check the existence. If it exists, we can
     delete the old list contents and split the whole
     path into the list and do post-processing.

     BUG: we may end up "entering into" a regular file.
     let's stat /oldpath/file/. instead.
     
     */

  cwd_ctx->user_context = context;
  cwd_ctx->change_cwd_cb = cwd_cb;
  if (path[0] == '/')
    {
      /* Case 1 */
      cwd_ctx->change_cwd_path = ssh_xstrdup(path);
    }
  else
    {
      /* Case 2 */
      cwd_ctx->change_cwd_path = ssh_sftp_cwd_add(path, cwd_ctx);
    }
  /* Append a /. into the directory to be checked, or if it already ends
     in a /, append just a . */
  cwd_ctx->path_to_be_checked = ssh_xmalloc(strlen(cwd_ctx->change_cwd_path) + 8);
  strcpy(cwd_ctx->path_to_be_checked, cwd_ctx->change_cwd_path);
  if (cwd_ctx->change_cwd_path[strlen(cwd_ctx->change_cwd_path) - 1] == '/')
    {
      cwd_ctx->path_to_be_checked[strlen(cwd_ctx->change_cwd_path)] = '.';
      cwd_ctx->path_to_be_checked[strlen(cwd_ctx->change_cwd_path) + 1] = '\0';
    }
  else
    {
      cwd_ctx->path_to_be_checked[strlen(cwd_ctx->change_cwd_path)] = '/';
      cwd_ctx->path_to_be_checked[strlen(cwd_ctx->change_cwd_path) + 1] = '.';
      cwd_ctx->path_to_be_checked[strlen(cwd_ctx->change_cwd_path) + 2] = '\0';
    }

  ssh_file_client_stat(cwd_ctx->file_client,
                       cwd_ctx->path_to_be_checked,
                       ssh_sftp_cwd_change_cb, cwd_ctx);
}

/* This function will try statting again if we failed because the freshly
   opened connection was not yet ready. */
void
ssh_sftp_cwd_timeout_cb(void *context)
{
  SshSftpCwdContext cwd_ctx = (SshSftpCwdContext)context;

  ssh_file_client_stat(cwd_ctx->file_client,
                       cwd_ctx->path_to_be_checked,
                       ssh_sftp_cwd_change_cb, cwd_ctx);
}
