/* ========================================================================== */
/*! \file
 * \brief Generic file handling and path checking functions
 *
 * Copyright (c) 2012-2015 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, functions return zero to indicate success
 * and a negative value to indicate an error.
 */


/* ========================================================================== */
/* Include headers */

#include "posix.h"  /* Include this first because of feature test macros */

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include "config.h"
#include "fileutils.h"
#include "main.h"


/* ========================================================================== */
/*! \addtogroup FILEUTILS
 *
 * This is a wrapper for the POSIX file handling stuff. It mainly does some
 * things for convenience and is not a real abstraction layer.
 *
 * \note
 * It can't be used from C++ code because the used POSIX API don't have a C++
 * binding.
 */
/* @{ */


/* ========================================================================== */
/* Dummy compare function for 'scandir()'
 *
 * \return
 * - Always 0 so that the sort order will be undefined.
 */

static int  fu_compar(const struct_posix_dirent**  a,
                      const struct_posix_dirent**  b)
{
   return(0);
}


/* ========================================================================== */
/*! \brief Check home directory path
 *
 * \param[in] homedir  String with home directory pathname to verify
 *
 * Accept only characters from the POSIX portable filename character set and '/'
 * for security reasons. Especially accept no space and '%' in the path.
 */

int  fu_check_homedir(const char*  homedir)
{
   int  res = 0;
   size_t  i;

   for(i = 0; i < strlen(homedir); ++i)
   {
      /* Check for alphanumeric characters */
      if('a' <= homedir[i] && 'z' >= homedir[i])  continue;
      if('A' <= homedir[i] && 'Z' >= homedir[i])  continue;
      /* Check for digits */
      if('0' <= homedir[i] && '9' >= homedir[i])  continue;
      /* Check for other allowed characters */
      if('.' == homedir[i])  continue;
      if('_' == homedir[i])  continue;
      if('-' == homedir[i])  continue;
      /* Check for slash */
      if('/' == homedir[i])  continue;

      /* Do not accept other characters */
      print_error("FU: Home directory path contains forbidden characters");
      res = -1;
      break;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Check whether file exist
 *
 * \param[in]   pathname   Pathname of file to open
 * \param[out]  state      Pointer to status buffer
 *
 * This is a wrapper for the POSIX \c stat() system call.
 *
 * This function check for a file from \e pathname and try to get its status.
 * If \e stat is \c NULL the status is thrown away.
 *
 * On success, status is written to location pointed to by \e stat
 */

int  fu_check_file(const char*  pathname, struct_posix_stat*  state)
{
   int  res = -1;
   struct_posix_stat  stat_dummy;

   if(NULL == state)  { res = posix_stat(pathname, &stat_dummy); }
   else  { res = posix_stat(pathname, state); }

   return(res);
}


/* ========================================================================== */
/*! \brief Open file
 *
 * \param[in]  pathname   Pathname of file to open
 * \param[out] filedesc   Pointer to file descriptor
 * \param[in]  mode       Mode for file to open
 * \param[in]  perm       Permissions for file to open (if it must be created)
 *
 * This is a wrapper for the POSIX \c open() system call that automatically
 * retry if the system call was interrupted by a signal.
 *
 * This function opens a file from \e pathname with the mode \e mode .
 * If a new file is created, the permissions from \e perm are used, otherwise
 * \e perm is ignored.
 */

int  fu_open_file(const char*  pathname, int*  filedesc, int  mode,
                  posix_mode_t  perm)
{
   int  res = 0;
   int  rv;

   /* Open file */
   do  { *filedesc = posix_open(pathname, mode, perm); }
   while(-1 == *filedesc && POSIX_EINTR == posix_errno);
   if (-1 == *filedesc)
   {
      /* print_error("FU: Cannot open file"); */
      res = -1;
   }
   else
   {
      /* Set 'FD_CLOEXEC' flag */
      do  { rv = posix_fcntl(*filedesc, POSIX_F_SETFD, POSIX_FD_CLOEXEC); }
      while(-1 == rv && POSIX_EINTR == posix_errno);
      if(-1 == rv)
      {
         print_error("FU: fcntl() failed");
         posix_close(*filedesc);
         res = -1;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Close file (and potentially associated I/O stream)
 *
 * \param[in] filedesc  Pointer to filedescriptor of file to close
 * \param[in] stream    Pointer to stream associated with \e filedesc
 *
 * This function first close the stream pointed to by \e stream and then the
 * filedescriptor pointed to by \e filedesc.
 *
 * \note
 * Both, \e filedesc and \e stream may be \c NULL and are ignored in this case.
 * If \e filedesc points to a location with value \c -1 it is ignored.
 * If \e stream points to a location containing a \c NULL pointer, it is
 * ignored.
 */

void  fu_close_file(int*  filedesc, FILE**  stream)
{
   /* Check for stream */
   if (NULL != stream)
   {
      if (NULL != *stream)  { fclose(*stream); }
      *stream = NULL;
   }

   /* Touch filedescriptor only if there was no stream */
   if (NULL == stream && NULL != filedesc)
   {
      if (-1 != *filedesc)  { posix_close(*filedesc); }
      *filedesc = -1;
   }

   /*
    * Attention:
    * Potential EINTR and EIO errors must be ignored
    * POSIX specify that both, the stream pointer for fclose() and the
    * filedescriptor for close() are in undefined state after such an error.
    * Therefore it is not allowed to retry the fclose()/close() calls.
    */
}


/* ========================================================================== */
/*! \brief Lock file for writing
 *
 * \param[in] filedesc  Descriptor of file to lock
 *
 * The file must be open for writing for this function to work.
 */

int  fu_lock_file(int  filedesc)
{
   int  res = -1;
   struct_posix_flock  fl;

   fl.l_type = POSIX_F_WRLCK;
   fl.l_whence = POSIX_SEEK_SET;
   fl.l_start = 0;
   fl.l_len = 0;
   res = posix_fcntl(filedesc, POSIX_F_SETLK, &fl);
   if(-1 == res)  { print_error("FU: Cannot lock file"); }
   else  res = 0;

   return(res);
}


/* ========================================================================== */
/*! \brief Unlink file
 *
 * \param[in] pathname  Pathname of file to unlink
 */

int  fu_unlink_file(const char*  pathname)
{
   return(posix_unlink(pathname));
}


/* ========================================================================== */
/*! \brief Assign I/O stream to open file
 *
 * \param[in]  filedesc  Filedescriptor to which the stream should be assigned
 * \param[out] stream    File stream that was assigned to file descriptor
 * \param[in]  mode      Access mode of the new stream
 */

int  fu_assign_stream(int  filedesc, FILE**  stream, const char*  mode)
{
   int  res = 0;

   *stream = posix_fdopen(filedesc, mode);
   if(NULL == *stream)
   {
      print_error("FU: Cannot assign I/O stream to file");
      res = -1;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Flush buffers of file
 *
 * \param[in]  filedesc  Filedescriptor of file
 * \param[out] stream    File stream assigned to file descriptor
 *
 * The filedescriptor \e filedesc must be valid, \e stream may be NULL if there
 * is no stream assigned to the filedescriptor.
 */

int  fu_sync(int  filedesc, FILE*  stream)
{
   int  res = 0;

   if(NULL != stream)
   {
      /* Flush user space buffers */
      do  { res = fflush(stream); }
      while(EOF == res && POSIX_EINTR == posix_errno);
   }

   if(!res)
   {
      /* Flush kernel buffers */
      do  { res = posix_fsync(filedesc); }
      while(-1 == res && POSIX_EINTR == posix_errno);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Read text file content and store it into memory buffer
 *
 * \param[in]   filedesc  Filedescriptor of file
 * \param[out]  buffer    Pointer to data buffer
 * \param[out]  len       Pointer to buffer size
 *
 * The filedescriptor \e filedesc must be valid.
 * The data is terminated with a \c NUL character, therefore the file must be
 * a text file without NUL characters in the content.
 *
 * \attention
 * The value written to \e len is the buffer size, not the data length!
 *
 * On success that caller is responsible to free the allocated buffer.
 */

int  fu_read_whole_file(int  filedesc, char**  buffer, size_t*  len)
{
   int  res = -1;
   size_t  i = 0;
   char*  p;
   posix_ssize_t  rv;

   *buffer = NULL;
   *len = 0;
   while(1)
   {
      if(i + (size_t) 1 >= *len)  /* At least one additional termination byte */
      {
         /* Allocate more memory in exponentially increasing chunks */
         if(!*len)  { *len = 256; }
         p = posix_realloc((void*) *buffer, *len *= (size_t) 2);
         if(NULL == p)  { break; }
         else  { *buffer = p; }
      }
      /* Read data from file */
      rv = posix_read(filedesc, &(*buffer)[i], *len - i - (size_t) 1);
      if(-1 == rv && POSIX_EINTR == posix_errno)  { continue; }
      if(-1 == rv)  { break; }
      else  { i += (size_t)  rv; }
      /* Check for EOF */
      if(!rv)  { (*buffer)[i] = 0;  res = 0;  break; }
   }

   /* Check for error */
   if(res)
   {
      posix_free((void*) *buffer);
      *buffer = NULL;
      *len = 0;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Write data block to filedescriptor
 *
 * \param[in]  filedesc  Filedescriptor of file
 * \param[in]  buffer    Pointer to data buffer
 * \param[in]  len       Data length in octets
 *
 * The filedescriptor \e filedesc must be valid.
 */

int  fu_write_to_filedesc(int  filedesc, const char*  buffer, size_t  len)
{
   int  res = -1;
   posix_ssize_t  rv;
   size_t  i = 0;

   while(i < len)
   {
      rv = posix_write(filedesc, (void*) &buffer[i], len - i);
      if(-1 == rv && POSIX_EINTR == posix_errno)  { continue; }
      if(-1 != rv)  { i += (size_t)  rv; }
      else  { break; }
   }
   if(len == i)  { res = 0; }
   if(res)  { print_error("FU: Writing data block to FD failed"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Delete directory tree
 *
 * \param[in] dir  Pathname of toplevel directory (to delete with all content)
 *
 * The parameter \e dir must point to a string with the absolute path.
 * A trailing slash is allowed and ignored.
 *
 * \attention
 * If the directory tree \e dir contains symbolic links, this function only
 * works if compiled to use the X/Open XSI extension of the operating system.
 */

int  fu_delete_tree(const char*  dir)
{
   int  res = -1;
   int  rv;
   int  num;
   size_t  len;
   char*  path;
   struct_posix_dirent**  content;
   struct_posix_stat  state;
   const char*  entry;
   char*  pathname;
   size_t  i;

   if(NULL != dir)
   {
      /* Check for absolute path */
      len = strlen(dir);
      if(len && '/' == dir[0])
      {
         /* Strip potential trailing slash of path */
         path = (char*) posix_malloc(len + (size_t) 1);
         if(NULL == path)
         {
            print_error("FU: Can't allocate memory for path");
         }
         else
         {
            strcpy(path, dir);
            if('/' == path[len - (size_t) 1])  { path[len - (size_t) 1] = 0; }
            /* Delete directory */
            num = posix_scandir(path, &content, NULL, fu_compar);
            if(0 > num)  { print_error("FU: Cannot scan directory"); }
            else
            {
               /* Init result to success. Report errors, but continue */
               res = 0;
               for(i = 0; i < (size_t) num; ++i)
               {
                  entry = content[i]->d_name;
                  /*
                   * A directory containing only "." and ".." is empty in terms
                   * of 'rmdir()' which means this function can delete it.
                   */
                  if(!strcmp(".", entry))  { continue; }
                  if(!strcmp("..", entry))  { continue; }
                  pathname = (char*) posix_malloc(strlen(path) + strlen(entry)
                                                  + (size_t) 2);
                  if(NULL == pathname)
                  {
                     print_error("FU: Cannot allocate memory for path");
                     res = -1;
                     continue;
                  }
                  strcpy(pathname, path);
                  strcat(pathname, "/");
                  strcat(pathname, entry);
                  /* printf("Pathname: %s\n", pathname); */
#if CFG_USE_XSI
                  /* This is the desired operation (don't follow symlinks) */
                  rv = posix_lstat(pathname, &state);
#else  /* CFG_USE_XSI */
                  /* Fallback that only works if there are no symlinks */
                  rv = posix_stat(pathname, &state);
#endif  /* CFG_USE_XSI */
                  if(rv)
                  {
                     print_error("FU: Cannot stat file");
                     res = -1;
                  }
                  else
                  {
                     /* Check whether entry is a directory */
                     if(POSIX_S_ISDIR(state.st_mode))
                     {
                        /* Yes => Recursively delete it */
                        rv = fu_delete_tree(pathname);
                        if(rv)  { res = -1; }
                     }
                     else
                     {
                        /* No => Delete file */
                        /* printf("Unlink file: %s\n", pathname); */
                        rv = fu_unlink_file(pathname);
                        if(rv)  { res = -1; }
                     }
                  }
                  posix_free((void*) pathname);
               }
               /* printf("Delete dir : %s\n", path); */
               rv = posix_rmdir(path);
               if(rv)  { res = -1; }
               /* Free memory allocated by scandir() */
               while(num--)  { posix_free((void*) content[num]); }
               posix_free((void*) content);
            }
            posix_free((void*) path);
         }
      }
   }

   return(res);
}


/* @} */

/* EOF */
