/* ========================================================================== */
/*! \file
 * \brief Handling of subscribed groups and already viewed articles
 *
 * Copyright (c) 2012-2020 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 <stdio.h>
#include <string.h>

#include "conf.h"
#include "config.h"
#include "core.h"
#include "encoding.h"
#include "fileutils.h"
#include "group.h"
#include "main.h"
#include "xdg.h"


/* ========================================================================== */
/*! \defgroup GROUPHANDLING GROUP: Usenet group handling
 *
 * Location of group information file: $XDG_CONFIG_HOME/$CFG_NAME/groupfile
 *
 * The content of the groupfile should be compatible with the de-facto standard
 * of the '~/.newsrc' file used by other newsreaders so that the contained data
 * can be shared with them if desired.
 */
/*! @{ */


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

/*! \brief Message prefix for GROUPHANDLING module */
#define MAIN_ERR_PREFIX  "GROUP: "


/*! \brief Permissions for group file */
#define GROUP_PERM  (posix_mode_t) (POSIX_S_IRUSR | POSIX_S_IWUSR)


/* ========================================================================== */
/* Variables */

static const char  groupfile_name[] = "groupfile";
static const char  tmpfile_name[] = "groupfile.new";


/* ========================================================================== */
/* Compare groups
 *
 * Only the group names up to ":" or "!" respectively are compared.
 *
 * \param[in] a  Line from groupfile
 * \param[in] b  Line from groupfile
 *
 * \return
 * - Negative value if \e a is less than \e b (\e a should be listed first)
 * - 0 if both group names are considered equal
 * - Negative value if \e a is greater than \e b (\e b should be listed first)
 */

static int  group_compar(const void*  a, const void*  b)
{
   int  res = 0;
   const char*  aa = *(const char**) a;
   const char*  bb = *(const char**) b;
   char*  x;
   char*  y;
   char*  p;

   x = (char*) posix_malloc(strlen(aa) + (size_t) 1);
   y = (char*) posix_malloc(strlen(bb) + (size_t) 1);
   if(NULL != x && NULL != y)
   {
      strcpy(x, aa);
      strcpy(y, bb);
      p = strchr(x, (int) ':');  if(NULL != p)  { *p = 0; }
      p = strchr(x, (int) '!');  if(NULL != p)  { *p = 0; }
      p = strchr(y, (int) ':');  if(NULL != p)  { *p = 0; }
      p = strchr(y, (int) '!');  if(NULL != p)  { *p = 0; }
      posix_errno = 0;
      res = strcoll(x, y);
      if(posix_errno)  { res = 0; }
   }
   posix_free((void*) x);
   posix_free((void*) y);

   return(res);
}


/* ========================================================================== */
/* Get groupfile pathname
 *
 * This function must be thread safe.
 * The caller is responsible to free the memory for the buffer on success.
 */

static int  group_get_pathname(const char**  pathname, const char*  filename)
{
   int  res = -1;
   int  rv;

   /* This requires the UI mutex */
   *pathname = xdg_get_confdir(CFG_NAME);
   if(NULL != *pathname)
   {
      rv = fu_create_path(*pathname, (posix_mode_t) POSIX_S_IRWXU);
      if(0 == rv)
      {
         /* Store scorefile pathname */
         rv = xdg_append_to_path(pathname, filename);
         if(0 == rv)
         {
            res = 0;
         }
      }
   }

   /* Free memory on error */
   if(0 != res)
   {
      PRINT_ERROR("Cannot create group file pathname");
      posix_free((void*) *pathname);
      *pathname = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Extract ranges of already read articles from string
 *
 * \param[out] list  Pointer to linked list containing ranges
 * \param[in]  s     Pointer to string containing ranges
 *
 * This function extracts article number ranges from string \e s and create a
 * linked list from the extracted data. On success a pointer to this list is
 * written to the location pointed to by \e rp .
 *
 * Format: 1-134,136,138-2500
 * White space is allowed to surround the numbers.
 * Consecutive and overlapping ranges are merged together.
 */

static int  group_extract_ranges(struct core_range**  list, const char*  s)
{
   int  res = 0;
   int  rv;
   const char*  p;
   size_t  len = 0;
   int  range_start = 1;
   core_anum_t  tmp;
   core_anum_t  start = 0;
   core_anum_t  end = 0;
   struct core_range*  list_start = NULL;
   struct core_range*  list_element;
   struct core_range*  current_element = NULL;

   while(*s)
   {
      /* Skip potential white space */
      while(' ' == *s || (char) 0x09 == *s )  { ++s; }
      /* Accept comma as field separator */
      while(',' == *s)  { ++s; }
      /* Skip potential white space */
      while(' ' == *s || (char) 0x09 == *s )  { ++s; }
      /* Check for end of string */
      if(!*s)  { break; }
      /* Digit(s) must follow now */
      p = s;
      if(enc_ascii_check_digit(s))  { res = -1;  break; }
      while(!enc_ascii_check_digit(++s));
      len = (size_t) (s - p);
      if((size_t) POSIX_INT_MAX < len)  { res = -1;  break; }
      rv = enc_convert_ascii_to_anum(&tmp, p, (int) len);
      if(rv)  { res = -1;  break; }
      /* Skip potential white space */
      while(' ' == *s || (char) 0x09 == *s )  { ++s; }
      if(range_start)
      {
         start = tmp;  range_start = 0;
         /* Check for single article range */
         if('-' != *s)  { end = start;  range_start = 1; }  else  { ++s; }
      }
      else
      {
         end = tmp;
         range_start = 1;
      }
      /* Store article range */
      if(range_start)
      {
         /* printf("Range: %lu-%lu\n", start, end); */
         res = group_article_range_constructor(&list_element, start, end, NULL);
         if(0 > res)  { break; }
         if(NULL == current_element)  { list_start = list_element; }
         else  { current_element->next = list_element; }
         current_element = list_element;
      }
   }
   /* Check for error */
   if(res)
   {
      PRINT_ERROR("Parsing article ranges in group file failed");
      group_article_range_destructor(&list_start);
   }
   else
   {
      /* Return linked list */
      *list = list_start;
   }

   return(res);
}


/* ========================================================================== */
/* Print one line to groupfile (using "~/.newsrc" style data format)
 *
 * \param[in] fs     File stream of groupfile to write
 * \param[in] group  Pointer to group state
 */

static int  group_print_entry(FILE*  fs,  struct core_groupstate*  group)
{
   int  res = 0;
   int  rv;
   struct core_range*  cr;
   unsigned long int  a;

   /* Print group name */
   rv = fprintf(fs, "%s: ", group->name);
   if(0 > rv)  { res = -1; }

   /* Print list of already read articles */
   if(!res)
   {
      if(CORE_ANUM_T_MAX > ULONG_MAX)
      {
         PRINT_ERROR("Value of CORE_ANUM_T_MAX is too large");
         res = -1;
      }
      else
      {
         cr = group->info;
         a = 0;
         while(NULL != cr)
         {
            if(a)  { rv = fprintf(fs, ", "); }
            if(0 > rv)  { res = -1;  break; }
            a = (unsigned long int) cr->first;
            rv = fprintf(fs, "%lu", a);
            if(0 > rv)  { res = -1;  break; }
            if(cr->last != cr->first)
            {
               a = (unsigned long int) cr->last;
               rv = fprintf(fs, "-%lu", a);
               if(0 > rv)  { res = -1;  break; }
            }
            cr = cr->next;
         }
      }
   }

   /* Terminate line */
   fprintf(fs, "\n");

   return(res);
}


/* ========================================================================== */
/* Export all groups to file (using "~/.newsrc" style data format)
 *
 * \param[in] num        Number of elements in group state array
 * \param[in] grouplist  Array of group states to merge
 * \param[in] fs         File stream of current groupfile
 * \param[in] fs_tmp     File stream of (temporary) output groupfile
 */

static int  group_export_groups(size_t  num, struct core_groupstate*  grouplist,
                                FILE*  fs, FILE*  fs_tmp)
{
   int  res = -1;

   char*  line = NULL;
   size_t  len = 0;
   posix_ssize_t  readlen;
   int  rv;
   char*  p;
   size_t*  inserted = NULL;
   size_t  i;
   int  error;
   int  modified;
   size_t  nlen = 0;

   inserted = (size_t*) posix_malloc(sizeof(size_t) * num);
   if(NULL != inserted)
   {
      /* Init insertion flags */
      for(i = 0; i < num; ++i)  { inserted[i] = 0; }

      /* Process lines of current groupfile */
      while(1)
      {
         /* Read line */
         readlen = posix_getline(&line, &len, fs);
         if(-1 == readlen)
         {
            if (POSIX_ENOMEM == posix_errno)
            {
               PRINT_ERROR("Cannot assign memory for group file parser");
               break;
            }
            else
            {
               /* Check for error */
               if(ferror(fs))
               {
                  PRINT_ERROR("Parse error in group file");
                  break;
               }
               /* Check for EOF */
               if(feof(fs))
               {
                  res = 0;
                  break;
               }
            }
         }
         if(0 >= readlen)  break;
         else
         {
            /* Replace potential CRs with SPACEes */
            for(i = 0; i < (size_t) readlen; ++i)
            {
               if(0x0D == (int) line[i])  { line[i] = ' '; }
            }
            /* Check whether one of the groups in array matches */
            error = 0;
            modified = 0;
            for(i = 0; i < num; ++i)
            {
               nlen = strlen(grouplist[i].name);
               if(!inserted[i] && !strncmp(line, grouplist[i].name, nlen))
               {
                  /* Verify that group name not only matches partly at beginning */
                  if(':' != line[nlen] && '!' != line[nlen])  { continue; }
                  /* Yes => Check whether group is unsubscribed */
                  p = strchr(line, (int) '!');
                  if(NULL != p)
                  {
                     /* Yes => Subscribe to group again and preserve state */
                     *p = ':';
                     /* Write modified line to new group file */
                     rv = fprintf(fs_tmp, "%s", line);
                     if(0 > rv)  { error = 1;  break; }
                  }
                  else
                  {
                     /* No => Replace line with current state */
                     res = group_print_entry(fs_tmp, &grouplist[i]);
                     if(res)  { error = 1;  break; }
                  }
                  inserted[i] = 1;
                  modified = 1;
                  break;
               }
            }
            if(error)  { break; }
            if(!modified)
            {
               /* Unsubscribe from group */
               p = strchr(line, (int) ':');
               if(NULL != p)  { *p = '!'; }
               rv = fprintf(fs_tmp, "%s", line);
               if(0 > rv)  break;
            }
         }
      }

      /* Append lines for unprocessed new groups */
      if(!res)
      {
         for(i = 0; i < num; ++i)
         {
            if(!inserted[i])
            {
               res = group_print_entry(fs_tmp, &grouplist[i]);
               if(res)  { break; }
            }
         }
      }
   }

   /* Release memory */
   posix_free((void*) line);
   posix_free((void*) inserted);

   return(res);
}


/* ========================================================================== */
/* Import subscribed groups from file (containing "~/.newsrc" style data)
 *
 * \param[out] num        Pointer to number of groups in list
 * \param[out] grouplist  Pointer to array of group name strings
 *
 * On success, the number of groups found in file \e fs is written to the
 * location pointed to by \e num .
 */

static int  group_import_groups(size_t*  num,
                                struct core_groupstate**  grouplist,
                                FILE*  fs)
{
   int  res = -1;
   char*  line;
   size_t  len = 0;
   posix_ssize_t  readlen;
   struct core_groupstate*  p;
   char*  c;
   struct core_range*  rp = NULL;

   *grouplist = NULL;
   *num = 0;

   while(1)
   {
      /* Read line */
      line = NULL;
      readlen = posix_getline(&line, &len, fs);
      if(-1 == readlen)
      {
         posix_free((void*) line);
         if (POSIX_ENOMEM == posix_errno)
         {
            PRINT_ERROR("Cannot allocate memory for group file parser");
            break;
         }
         else
         {
            /* Check for error */
            if(ferror(fs))
            {
               PRINT_ERROR("Parse error in group file");
               break;
            }
            /* Check for EOF */
            if(feof(fs))
            {
               res = 0;
               break;
            }
         }
      }
      if(0 >= readlen)  break;
      else
      {
         /* Verify line */
         if(enc_ascii_check(line))
         {
            /* Invalid encoding => Ignore line */
            posix_free((void*) line);
            continue;
         }
         /* Extract group name */
         if(strchr(line, (int) '!'))
         {
            /* Ignore unsubscribed groups */
            posix_free(line);
            continue;
         }
         c = strchr(line, (int) ':');
         if(NULL == c)
         {
            /* Invalid format => Ignore line */
            posix_free((void*) line);
            continue;
         }
         /* Terminate group name by replacing ':' with NUL */
         *c = 0;
         /* Add group name to list */
         p = (struct core_groupstate*)
             posix_realloc((void*) *grouplist,
                           (*num + (size_t) 1) * sizeof(**grouplist));
         if(NULL == p)
         {
            PRINT_ERROR("Cannot allocate memory for group list");
            posix_free((void*) line);
            break;
         }
         else
         {
            *grouplist = p;
            line[readlen - (posix_ssize_t) 1] = 0;
            (*grouplist)[(*num)].name = line;
            (*grouplist)[(*num)].info = NULL;
            (*grouplist)[(*num)].last_viewed = 0;
         }
         /* Get numbers of already read articles (c still points to ':') */
         if(!group_extract_ranges(&rp, ++c))
         {
            /* Store range list */
            (*grouplist)[(*num)].info = rp;
         }
         /* Increment number of groups found */
         ++*num;
      }
   }

   /* Release memory on error */
   if(res)
   {
      /* Destroy group list */
      group_destroy_list(num, grouplist);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Initialize group handling
 *
 * This function prepares the groupfile using the following algorithm:
 * - Check whether \c CONF_NEWSRC is configured
 * - If not configured abort and return, otherwise continue
 * - Check whether pathname configured with \c CONF_NEWSRC is valid
 * - If not valid abort and return
 * - Rename current \c groupfile to \c groupfile.old
 * - Copy pathname configured with \c CONF_NEWSRC to \c groupfile
 */

void  group_init(void)
{
   const char*  newsrc = config[CONF_NEWSRC].val.s;
   const char*  grouppathname = NULL;
   char*  oldgrouppathname = NULL;
   int  rv;
   struct_posix_stat  state;
   int  fd = -1;
   char*  data = NULL;
   size_t  len;

   if(strlen(newsrc))
   {
      if(main_debug)
      {
         printf("%s: %sImport external newsrc: %s\n",
                CFG_NAME, MAIN_ERR_PREFIX, newsrc);
      }
      rv = posix_stat(newsrc, &state);
      if(rv)  { PRINT_ERROR("Cannot stat newsrc file"); }
      else if(POSIX_S_ISREG(state.st_mode))
      {
         rv = group_get_pathname(&grouppathname, groupfile_name);
         if(!rv)
         {
            /* Read newsrc file */
            rv = fu_open_file(newsrc, &fd, POSIX_O_RDWR, (posix_mode_t) 0);
            if(!rv)
            {
               rv = fu_lock_file(fd);
               if(!rv)
               {
                  rv = fu_read_whole_file(fd, &data, &len);
               }
               fu_close_file(&fd, NULL);
               if(!rv)
               {
                  oldgrouppathname =
                     (char*) posix_malloc(strlen(grouppathname) + (size_t) 5);
                  if(NULL == oldgrouppathname)
                  {
                     PRINT_ERROR("Cannot allocate memory for pathname");
                  }
                  else
                  {
                     strcpy(oldgrouppathname, grouppathname);
                     strcat(oldgrouppathname, ".old");
                     rv = posix_rename(grouppathname, oldgrouppathname);
                     if(rv)
                     {
                        PRINT_ERROR("Renaming groupfile failed");
                     }
                     else
                     {
                        rv = fu_open_file(grouppathname, &fd,
                                          POSIX_O_WRONLY | POSIX_O_CREAT,
                                          GROUP_PERM);
                        if(!rv)
                        {
                           rv = fu_lock_file(fd);
                           if(!rv)
                           {
                              len = strlen(data);
                              rv = fu_write_to_filedesc(fd, data, len);
                           }
                           fu_close_file(&fd, NULL);
                        }
                     }
                  }
               }
            }
         }
      }
      if(rv)
      {
         PRINT_ERROR("Importing newsrc failed, using local groupfile");
      }
   }

   /* Release memory */
   posix_free((void*) data);
   posix_free((void*) oldgrouppathname);
   posix_free((void*) grouppathname);
}


/* ========================================================================== */
/*! \brief Shutdown group handling
 *
 * This function copy the groupfile back to the location configured by
 * \c CONF_NEWSRC .
 */

void  group_exit(void)
{
   const char*  newsrc = config[CONF_NEWSRC].val.s;
   const char*  grouppathname = NULL;
   char*  tmppathname = NULL;
   int  rv;
   int  fd = -1;
   char*  data = NULL;
   size_t  len;

   if(strlen(newsrc))
   {
      /* Read groupfile */
      rv = group_get_pathname(&grouppathname, groupfile_name);
      if(!rv)
      {
         rv = fu_open_file(grouppathname, &fd, POSIX_O_RDWR,
                           (posix_mode_t) 0);
         if(!rv)
         {
            rv = fu_lock_file(fd);
            if(!rv)
            {
               rv = fu_read_whole_file(fd, &data, &len);
            }
            fu_close_file(&fd, NULL);
            if(!rv)
            {
               /* Write newsrc file */
               if(main_debug)
               {
                  printf("%s: %sExport to external newsrc: %s\n",
                         CFG_NAME, MAIN_ERR_PREFIX, newsrc);
               }
               tmppathname = (char*) posix_malloc(strlen(newsrc) + (size_t) 5);
               if(NULL == tmppathname)
               {
                  PRINT_ERROR("Cannot allocate memory for pathname");
               }
               else
               {
                  strcpy(tmppathname, newsrc);
                  strcat(tmppathname, ".new");
                  rv = fu_open_file(tmppathname, &fd,
                                    POSIX_O_WRONLY | POSIX_O_CREAT, GROUP_PERM);
                  if(!rv)
                  {
                     rv = fu_lock_file(fd);
                     if(!rv)
                     {
                        len = strlen(data);
                        rv = fu_write_to_filedesc(fd, data, len);
                        if(rv)  { rv = fu_sync(fd, NULL); }
                        if(rv)
                        {
                           PRINT_ERROR("Writing data to newsrc file failed");
                        }
                        else
                        {
                           rv = posix_rename(tmppathname, newsrc);
                           if(rv)
                           {
                              PRINT_ERROR("Renaming new newsrc file failed");
                           }
                        }
                     }
                     fu_close_file(&fd, NULL);
                  }
               }
            }
         }
      }
      if(rv)  { PRINT_ERROR("Exporting groupfile data to newsrc failed"); }
   }

   /* Release memory */
   posix_free((void*) data);
   posix_free((void*) tmppathname);
   posix_free((void*) grouppathname);
}


/* ========================================================================== */
/*! \brief Article range constructor
 *
 * \param[out] range  Pointer to article range structure
 * \param[in]  start  First article in range
 * \param[in]  end    Last article in range
 * \param[in]  next   Pointer to next article range structure in linked list
 *
 * If success (zero) is returned, the caller is responsible for releasing the
 * memory allocated for the article range object.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  group_article_range_constructor(struct core_range**  range,
                                     core_anum_t  start, core_anum_t  end,
                                     struct core_range*  next)
{
   int  res = 0;

   *range = (struct core_range*) posix_malloc(sizeof(struct core_range));
   if (NULL == *range)  { res = -1; }
   else
   {
      (*range)->first = start;
      (*range)->last = end;
      (*range)->next = next;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Destructor for linked list of article ranges
 *
 * \param[in] list  Pointer to linked list of article ranges
 */

void  group_article_range_destructor(struct core_range**  list)
{
   struct core_range*  tmp_element;
   struct core_range*  current_element = NULL;

   /* Delete linked list */
   current_element = *list;
   while(NULL != current_element)
   {
      tmp_element = current_element->next;
      posix_free((void*) current_element);
      current_element = tmp_element;
   }
   *list = NULL;
}


/* ========================================================================== */
/*! \brief Destroy list of subscribed groups
 *
 * \param[in] num        Pointer to number of groups in list
 * \param[in] grouplist  Pointer to array of group information structures
 *
 * If \e grouplist is \c NULL , this function do nothing.
 * On success, \e num is zero and \e grouplist is \c NULL after return.
 */

void  group_destroy_list(size_t*  num, struct core_groupstate**  grouplist)
{
   if(NULL == grouplist)  { return; }

   while((*num)--)
   {
      posix_free((void*) (*grouplist)[*num].name);
      group_article_range_destructor(&(*grouplist)[*num].info);
   }
   /* Unsigned integer under/overflow is intended and has defined behaviour */
   ++(*num);
   posix_free((void*) *grouplist);
   *grouplist = NULL;
}


/* ========================================================================== */
/*! \brief Get list with subscribed groups
 *
 * \param[out] num        Pointer to number of groups in list
 * \param[out] grouplist  Pointer to array of group information structures
 *
 * If success (zero) is returned, the caller is responsible for releasing the
 * memory allocated for the group list object.
 * The destructor \e group_destroy_list() should be used for this purpose.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  group_get_list(size_t*  num, struct core_groupstate**  grouplist)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   const char*  grouppathname = NULL;

   /* Open and lock group file */
   res = group_get_pathname(&grouppathname, groupfile_name);
   if(!res)
   {
      res = fu_open_file(grouppathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                         GROUP_PERM);
      if(!res)  { res = fu_lock_file(fd); }
      if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
   }

   /* Import group list */
   if(!res)  { res = group_import_groups(num, grouplist, fs); }

   /* Close group file (this will also release the lock) */
   fu_close_file(&fd, &fs);

   /* Release memory for file pathname */
   posix_free((void*) grouppathname);

   return(res);
}


/* ========================================================================== */
/*! \brief Store list with subscribed groups
 *
 * \param[in] num        Number of elements in group information array
 * \param[in] grouplist  Array of group information structures
 *
 * If \e num is zero, \e grouplist is ignored and the configuration is cleared.
 * All group information elements in the array must be unique, that means it is
 * not allowed to have multiple entries with the same group name.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  group_set_list(size_t  num, struct core_groupstate*  grouplist)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   int  fd_tmp = -1;
   FILE*  fs_tmp = NULL;
   const char*  grouppathname = NULL;
   const char*  tmppathname = NULL;

   /* Get file pathnames */
   res = group_get_pathname(&grouppathname, groupfile_name);
   if(!res)  { res = group_get_pathname(&tmppathname, tmpfile_name); }

   /* Open and lock group file */
   if(!res)
   {
      res = fu_open_file(grouppathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                         GROUP_PERM);
      if(!res)  { res = fu_lock_file(fd); }
      if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
   }

   /* Open temporary file for new group list */
   if(!res)
   {
      res = fu_open_file(tmppathname, &fd_tmp,
                         POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
                         GROUP_PERM);
      /*
       * Because we have the lock for the group file, it is allowed to
       * assume that no other instance of the program currently use the
       * temporary filename.
       */
      if(!res)  { res = fu_assign_stream(fd_tmp, &fs_tmp, "w"); }
   }

   /* Export group list to temporary file */
   if(!res && num)  { res = group_export_groups(num, grouplist, fs, fs_tmp); }

   /* Flush stream of temporary file*/
   if(!res)  { res = fu_sync(fd_tmp, fs_tmp); }

   /* Rename temporary file to group file */
   if(!res)  { res = posix_rename(tmppathname, grouppathname); }

   /* Unlink temporary file after error */
   if(res)
   {
      PRINT_ERROR("Failed to store group file");
      if(tmppathname)  { (void) fu_unlink_file(tmppathname); }
   }

   /* The tmp file must be removed before releasing the group file lock! */
   fu_close_file(&fd_tmp, &fs_tmp);

   /* Close group file (this will also release the lock) */
   fu_close_file(&fd, &fs);

   /* Release memory for file pathnames */
   posix_free((void*) tmppathname);
   posix_free((void*) grouppathname);

   return(res);
}


/* ========================================================================== */
/*! \brief Alphabetically sort list with subscribed groups
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  group_sort_list(void)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   int  fd_tmp = -1;
   FILE*  fs_tmp = NULL;
   const char*  grouppathname = NULL;
   const char*  tmppathname = NULL;
   char*  line;
   size_t  len = 0;
   posix_ssize_t  readlen;
   const char**  array = NULL;
   const char**  array_tmp;
   size_t  array_len = 0;
   int  rv;
   size_t  i;

   /* Get file pathnames */
   res = group_get_pathname(&grouppathname, groupfile_name);
   if(!res)  { res = group_get_pathname(&tmppathname, tmpfile_name); }

   /* Open and lock group file */
   if(!res)
   {
      res = fu_open_file(grouppathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                         GROUP_PERM);
      if(!res)  { res = fu_lock_file(fd); }
      if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
   }

   /* Open temporary file for new group list */
   if(!res)
   {
      /*
       * Because we have the lock for the group file, it is allowed to
       * assume that no other instance of the program currently use the
       * temporary filename.
       */
      res = fu_open_file(tmppathname, &fd_tmp,
                         POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
                         GROUP_PERM);
      if(!res)  { res = fu_assign_stream(fd_tmp, &fs_tmp, "w"); }
   }

   /* Read groupfile lines */
   if(!res)
   {
      res = -1;
      while(1)
      {
         line = NULL;
         readlen = posix_getline(&line, &len, fs);
         if(-1 == readlen)
         {
            posix_free((void*) line);
            if (POSIX_ENOMEM == posix_errno)
            {
               PRINT_ERROR("Cannot allocate memory for group data");
               break;
            }
            else
            {
               /* Check for error */
               if(ferror(fs))
               {
                  PRINT_ERROR("Parse error in group file");
                  break;
               }
               /* Check for EOF */
               if(feof(fs))
               {
                  if(NULL != array)  { res = 0; }
                  break;
               }
            }
         }
         if(0 >= readlen)  break;
         else
         {
            /* Create array of line pointers */
            array_tmp = (const char**)
                        posix_realloc(array, (array_len + (size_t) 1)
                                      * (sizeof(const char*)));
            if(NULL == array_tmp)  { posix_free((void*) line);  break; }
            else
            {
               array = array_tmp;
               array[array_len++] = line;
            }
         }
      }
   }

   /* Sort lines */
   if(!res)
   {
      qsort((void*) array, array_len, sizeof(const char*), group_compar);
      for(i = 0; i < array_len; ++i)
      {
         /* Write modified line to new group file */
         rv = fprintf(fs_tmp, "%s", array[i]);
         if(0 > rv)  { res = -1;  break; }
      }
   }

   /* Release memory for array of lines */
   while(array_len)  { posix_free((void*) array[--array_len]); }
   posix_free((void*) array);

   /* Flush stream of temporary file*/
   if(!res) { res = fu_sync(fd_tmp, fs_tmp); }

   /* Rename temporary file to group file */
   if(!res)  { res = posix_rename(tmppathname, grouppathname); }

   /* Unlink temporary file after error */
   if(res)
   {
      PRINT_ERROR("Failed to store group file");
      if(tmppathname)  { (void) fu_unlink_file(tmppathname); }
   }

   /* The tmp file must be removed before releasing the group file lock! */
   fu_close_file(&fd_tmp, &fs_tmp);

   /* Close group file (this will also release the lock) */
   fu_close_file(&fd, &fs);

   /* Release memory for file pathnames */
   posix_free((void*) tmppathname);
   posix_free((void*) grouppathname);

   return(res);
}


/* ========================================================================== */
/*! \brief Add subscribed group
 *
 * \param[in] group  Pointer to group information
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  group_add(struct core_groupstate*  group)
{
   int  res = -1;
   int  fd = -1;
   FILE*  fs = NULL;
   int  fd_tmp = -1;
   FILE*  fs_tmp = NULL;
   const char*  grouppathname = NULL;
   const char*  tmppathname = NULL;
   char*  line = NULL;
   size_t  len = 0;
   posix_ssize_t  readlen;
   int  rv;
   size_t  nlen;
   char*  p;
   int  found = 0;

   /* Get file pathnames */
   res = group_get_pathname(&grouppathname, groupfile_name);
   if(!res)  { res = group_get_pathname(&tmppathname, tmpfile_name); }

   /* Open and lock group file */
   if(!res)
   {
      res = fu_open_file(grouppathname, &fd, POSIX_O_RDWR | POSIX_O_CREAT,
                         GROUP_PERM);
      if(!res)  { res = fu_lock_file(fd); }
      if(!res)  { res = fu_assign_stream(fd, &fs, "r"); }
   }

   /* Open temporary file for new group list */
   if(!res)
   {
      res = fu_open_file(tmppathname, &fd_tmp,
                         POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
                         GROUP_PERM);
      /*
       * Because we have the lock for the group file, it is allowed to
       * assume that no other instance of the program currently use the
       * temporary filename.
       */
      if(!res)  { res = fu_assign_stream(fd_tmp, &fs_tmp, "w"); }
   }

   /* Copy current data to temporary file and append new group if not present */
   if(!res)
   {
      res = -1;
      while(1)
      {
         /* Read line */
         readlen = posix_getline(&line, &len, fs);
         if(-1 == readlen)
         {
            if (POSIX_ENOMEM == posix_errno)
            {
               PRINT_ERROR("Cannot assign memory to read group file");
            }
            else
            {
               /* Check for error */
               if(ferror(fs))
               {
                  PRINT_ERROR("Reading from group file failed");
                  break;
               }
               /* Check for EOF */
               if(feof(fs))
               {
                  res = 0;
                  break;
               }
            }
         }
         if(0 >= readlen)  break;
         else
         {
            /* Compare with new group */
            nlen = strlen(group->name);
            if(!strncmp(line, group->name, nlen))
            {
               /* Verify that group name not only matches partly at beginning */
               if(':' == line[nlen] || '!' == line[nlen])
               {
                  found = 1;
                  /* Check whether group is unsubscribed */
                  p = strchr(line, (int) '!');
                  if(NULL != p)
                  {
                     /* Yes => Subscribe to group again and preserve state */
                     *p = ':';
                  }
               }
            }
            /* Write line */
            rv = fprintf(fs_tmp, "%s", line);
            if(0 > rv)  break;
         }
      }
      /* Append new group if it was not found */
      if(!res && !found)  { res = group_print_entry(fs_tmp, group); }
   }

   /* Flush stream of temporary file*/
   if(!res) { res = fu_sync(fd_tmp, fs_tmp); }

   /* Rename temporary file to group file */
   if(!res)  { res = posix_rename(tmppathname, grouppathname); }

   /* Unlink temporary file after error */
   if(res)
   {
      PRINT_ERROR("Failed to store group file");
      if(tmppathname)  { (void) fu_unlink_file(tmppathname); }
   }

   /* The tmp file must be removed before releasing the group file lock! */
   fu_close_file(&fd_tmp, &fs_tmp);

   /* Close group file (this will also release the lock) */
   fu_close_file(&fd, &fs);

   /* Release memory for file pathnames */
   posix_free((void*) line);
   posix_free((void*) tmppathname);
   posix_free((void*) grouppathname);

   return(res);
}


/* ========================================================================== */
/*! \brief Delete group states
 *
 * This is necessary after changing the server because every server use its
 * own article numbers (on which our group states are based).
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */
int  group_reset_states(void)
{
   int  res = -1;
   size_t  num;
   struct core_groupstate*  gs;
   size_t  i;

   /* Get group list with states */
   res = group_get_list(&num, &gs);
   if(!res)
   {
      /* Strip all states leaving only the group names */
      for(i = 0; i < num; ++i)  { group_article_range_destructor(&gs[i].info); }
      group_set_list(0, NULL);
      res = group_set_list(num, gs);
      group_destroy_list(&num, &gs);
   }

   return(res);
}


/*! @} */

/* EOF */
