/* ========================================================================== */
/*! \file
 * \brief Generic file handling and path checking functions
 *
 * Copyright (c) 2012-2021 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"


/* ========================================================================== */
/* Constants */

/*! \brief Message prefix for FILEUTILS module */
#define MAIN_ERR_PREFIX  "FUTIL: "


/* ========================================================================== */
/*! \defgroup FILEUTILS FUTIL: POSIX file handling
 *
 * 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 doesn't has 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 path
 *
 * \param[in] path  String with path or 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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

int  fu_check_path(const char*  path)
{
   int  res = 0;
   size_t  i;

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

      /* Do not accept other characters */
      PRINT_ERROR("Path or pathname contains forbidden characters");
      res = -1;
      break;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Create path
 *
 * \param[in] path  String with path to create
 * \param[in] perm  Permissions for directories to create
 *
 * \attention \e path must not be \c NULL and must be an absolute path.
 *
 * \c mkdir() is called for every component of the path. If \e path does not
 * end with a slash, the last component is not treated as a directory to
 * create.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

int  fu_create_path(const char*  path, posix_mode_t  perm)
{
   int  res = 0;
   size_t  pos = 1;
   char*  p;
   char*  pcomp = NULL;
   size_t  len;
   int  rv;

   if(NULL == path || '/' != path[0])
   {
      goto error;
   }
   /* printf("Path to create: %s\n", path); */

   while (0 != path[pos])
   {
      /* Extract next path component */
      p = strchr(&path[pos], (int) '/');
      if(NULL != p)
      {
         len = (size_t) (p - &path[pos]);
         pos += len + (size_t) 1;  /* New position after slash */
      }
      else
      {
         /* Last component without trailing slash */
         len = (size_t) (&path[strlen(path)] - &path[pos]);
         pos += len;  /* New position on NUL termination */
         /* Verify NUL termination to ensure that loop terminates */
         if(0 != path[pos])
         {
            PRINT_ERROR("Infinite loop detected (bug)");
            goto error;
         }
      }

      pcomp = (char*) posix_malloc(pos + (size_t) 1);
      if(NULL == pcomp)
      {
         PRINT_ERROR("Can't allocate memory for path component");
         goto error;
      }
      memcpy((void*) pcomp, (void*) path, pos);
      pcomp[pos] = 0;

      /* Remove potential trailing slash */
      /* Note: According to POSIX.1 it should work with the slash too */
      if('/' == pcomp[pos - (size_t) 1])
      {
         pcomp[pos - (size_t) 1] = 0;
      }
      /* printf("Directory to create: %s\n", pcomp); */

      /* Create configuration directory, if it doesn't exist */
      rv = posix_mkdir(pcomp, perm);
      if(!(0 == rv || (-1 == rv && POSIX_EEXIST == posix_errno)))
      {
         goto error;
      }

      posix_free((void*) pcomp);
   }

exit:
   return(res);

error:
   PRINT_ERROR("Cannot create directory for path");
   res = -1;
   goto exit;
}


/* ========================================================================== */
/*! \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
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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("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("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); }
   }
   if (NULL != filedesc)  { *filedesc = -1; }

   /*
    * Attention:
    * Potential EINTR and EIO errors must be ignored
    * POSIX specifies 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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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("Cannot lock file"); }
   else  res = 0;

   return(res);
}


/* ========================================================================== */
/*! \brief Unlink file
 *
 * \param[in] pathname  Pathname of file to unlink
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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

   *stream = posix_fdopen(filedesc, mode);
   if(NULL == *stream)
   {
      PRINT_ERROR("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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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 Read data block to filedescriptor
 *
 * \param[in]      filedesc  Filedescriptor of file
 * \param[out]     buffer    Pointer to data buffer
 * \param[in,out]  len       Pointer to data length in octets
 *
 * The filedescriptor \e filedesc must be valid.
 *
 * \attention
 * The caller must set the value \e len points to to the size of the buffer.
 * On success the value is overwritten with the number of octets read.
 * A buffer size of zero is treated as error.
 *
 * \return
 * - 0 on success (at least one octet was written to \e buffer)
 * - Nonzero on error (or EOF before the first octet was read)
 */

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

   while(i < *len)
   {
      rv = posix_read(filedesc, (void*) &buffer[i], *len - i);
      if((posix_ssize_t) -1 == rv && POSIX_EINTR == posix_errno)  { continue; }
      if(!rv)  { break; }  /* EOF */
      if((posix_ssize_t) -1 != rv)  { i += (size_t)  rv; }  else  { break; }
   }
   if(-1 == rv)  { PRINT_ERROR("Error while reading from FD"); }
   if(i)
   {
      *len = i;
      res = 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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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((posix_ssize_t) -1 == rv && POSIX_EINTR == posix_errno)  { continue; }
      if((posix_ssize_t) -1 != rv)  { i += (size_t)  rv; }  else  { break; }
   }
   if(len == i)  { res = 0; }
   if(res)  { PRINT_ERROR("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.
 *
 * \return
 * - Zero on success
 * - Negative value on error
 */

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("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("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("Cannot allocate memory for path");
                     res = -1;
                     continue;
                  }
                  strcpy(pathname, path);
                  strcat(pathname, "/");
                  strcat(pathname, entry);
                  /* printf("Pathname: %s\n", pathname); */
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI
                  /* This is the desired operation (not follow symlinks) */
                  rv = posix_lstat(pathname, &state);
#else  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI */
                  /* Fallback that only works if there are no symlinks */
                  rv = posix_stat(pathname, &state);
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI */
                  if(rv)
                  {
                     PRINT_ERROR("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 */
