/*
 * nsync (newsrc sync)
 * -------------------
 * 
 * version 1.1 - Oct 4, 1998
 * author: Frederik Ramm <frederik@remote.org>
 * this version and future updates available from 
 * http://www.remote.org/frederik/projects/software/
 * 
 * to build: cc -o nsync nsync.c
 * 
 * This program allows you to keep track of read and unread Usenet articles
 * across multiple servers.
 * 
 * Since all decent newsreaders store information about read articles in 
 * their .newsrc file by local article IDs, .newsrc files are not portable
 * as-is. 
 * 
 * Using nsync, you can translate the numbers of read articles in your
 * .newsrc file into (external) message IDs as recorded in the history file
 * of your news server. 
 * 
 * On the other side, you can again use nsync to re-translate these
 * message IDs into article IDs using the history file of the local news
 * server, and update your .newsrc file accordingly. 
 * 
 * A typical use would be (assuming that file access permissions as well 
 * as UUCP execute permissions are set correctly):
 * nsync | uux 'otherhost!nsync -n /home/username/.newsrc -m'
 * This reads your current .newsrc, translates it into message IDs, 
 * transfers them to otherhost and there marks the articles as read in the 
 * file /home/username/.newsrc.
 *
 * (Of course, if you don't want to use UUCP, you can also create a transfer
 * file, copy it, and process it 'manually' on the other side, or something
 * like 
 * nsync | ssh otherhost "nsync -m").
 * 
 * 
 * version history:
 * v1    released 4th Oct 1998
 * v1.1  released same day after correcting a bug in insertRange which 
 *       could cause a chain of article ranges at the and to be omitted
 *       (bug hunter: Sven Paulus <sven@karlsruhe.org> - that was an 
 *       obvious one :-)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unistd.h>

/* default if not overridden by -n */
#define NEWSRC  ".newsrc"

/* default if not overridden by -h */
#define HISTORY "/var/lib/news/history"

#define LINELENGTH 10240

/* struct for linked list for article ranges within a group */
struct range {
   long int from;
   long int to;
   struct range *next;
};

/* struct for newsgroup descriptor */
struct newsgroup {
   char *name;                /* NG name */
   long int min;              /* smallest article number read */
   long int max;              /* largest article number read */
   struct range *firstRange;  /* pointer to first element of range list */
   struct range *lastRange;   /* pointer to last element of range list */
   struct newsgroup *next;    /* pointer to next newsgroup struct */
};

struct newsgroup *ngHead = NULL, *ngTail = NULL;
int mergeMode = 0;
int count;
char **idList;

/* prototypes */
struct newsgroup *retrieveGroup(char *);
struct newsgroup *insertGroup(char *);
void              appendRange(struct newsgroup *, struct range *);
void              parseHistory(char *filename);
int               writeNewsrc(char *filename);
void              usageMessage();
int               readNewsrc(char *filename);
int               readIDs();
int               isIdRead(char *msgId);


/* --------------------------------------------------------------------------
 * here we go
 * -------------------------------------------------------------------------- */

int main(int argc, char **argv) {

   char *historyfile = HISTORY;
   char *newsrcfile = NEWSRC;
   int i;

   for (i=1; i<argc; i++) {
      if (*argv[i] == '-') {
         switch(*(argv[i]+1)) {
         case 'n': newsrcfile = argv[i]+2;
                   if (!*newsrcfile) newsrcfile = argv[++i];
                   break;
         case 'h': historyfile = argv[i]+2;
                   if (!*historyfile) historyfile = argv[++i];
                   break;
         case 'm': mergeMode = 1;
                   break;
         default:  usageMessage();
                   return(0);
         }
      } else {
         usageMessage();
         return(0);
      }
   }

   if (!readNewsrc(newsrcfile)) {
      fprintf(stderr, "Cannot read newsrc file '%s'\n", newsrcfile);
      return(1);
   }
      
   if (!ngHead) {
      fprintf(stderr, "No subscribed newsgroups in newsrc file '%s'\n", newsrcfile);
      return(1);
   }

   if (mergeMode && !readIDs()) {
      fprintf(stderr, "Cannot read article IDs from stdin\n");
      return(1);
   }
      
   parseHistory(historyfile);

   if (mergeMode && !writeNewsrc(newsrcfile)) {
      fprintf(stderr, "Cannot write newsrc file '%s'\n", newsrcfile);
      return(1);
   }

   return(0);
}

void usageMessage() {

   printf("Usage: nsync [-h history-file] [-n newsrc-file] [-m]\n\n");
   printf("In merge mode (-m), reads article MsgID list from stdin and updates .newsrc\n");
   printf("accordingly; otherwise, evaluates .newsrc and writes MsgID list to stdout.\n");
   printf("\nCompiled-in defaults: -h %s, -n %s\n", HISTORY, NEWSRC);

}

int readNewsrc(char *filename) {

   FILE *newsrc;
   char buffer[LINELENGTH];
   char *range, *colon;

   newsrc = fopen(filename, "r");
   if (!newsrc) return(0);

   while (!feof(newsrc)) {

      fgets(buffer, sizeof(buffer)-1, newsrc);
      if (buffer[strlen(buffer)-1] == '\n') buffer[strlen(buffer)-1] = 0;
      if ((colon = strchr(buffer, ':'))) {

         struct newsgroup *thisGroup;
         *colon = 0;
         colon+=2;
         range = strtok(colon, ",");
         /* if (range)  */
         thisGroup = insertGroup(buffer);
         while (range) {
            struct range *thisRange = 
                   (struct range *) malloc(sizeof(struct range));
            char *minus = strchr(range, '-');
            if (minus) {
               *(minus++) = 0;
               thisRange->from = atol(range); thisRange->to = atol(minus);
            } else {
               thisRange->from = atol(range); thisRange->to = thisRange->from;
            }
            appendRange(thisGroup, thisRange);
            range = strtok(NULL, ",");
         }

      }
   }

   fclose(newsrc);
   return(1);

}
   
int writeNewsrc(char *filename) {

   FILE *newsrc, *outfile;
   char buffer[LINELENGTH];
   char *colon;
   char *oldfilename = (char *) malloc(strlen(filename)+8);
   
   sprintf(oldfilename, "%s.backup", filename);
   if (rename(filename, oldfilename)) {
      return(0);
   }
   
   newsrc = fopen(oldfilename, "r");
   if (!newsrc) return(0);

   outfile = fopen(filename, "w");
   if (!outfile) return(0);

   while (!feof(newsrc)) {

      fgets(buffer, sizeof(buffer)-1, newsrc);
      if (feof(newsrc)) break;
      if (buffer[strlen(buffer)-1] == '\n') buffer[strlen(buffer)-1] = 0;

      if ((colon = strchr(buffer, ':'))) {

         /* This line contains a subscribed-to newsgroup. We replace
          * the list of read articles.
          */ 

         struct newsgroup *thisGroup;
         *colon = 0;

         fprintf(outfile, "%s: ", buffer);
         thisGroup = retrieveGroup(buffer);

         if (!thisGroup) {
   
            /* oops! We have no information about this group - 
             * ignore it
             */
            fprintf(outfile, "%s\n", colon+2);

         } else {

            struct range *currentRange = thisGroup->firstRange;
            while (currentRange) {
               fprintf(outfile, "%ld", currentRange->from);
               if (currentRange->from != currentRange->to) {
                  fprintf(outfile, "-%ld", currentRange->to);
               }
               currentRange=currentRange->next;
               if (currentRange) {
                  fprintf(outfile, ",");
               } else {
                  break;
               }
            }
            fprintf(outfile, "\n");
         }
         
      } else {

         /* The old .newsrc line does not contain a colon - 
          * probably unsubscribed newsgroup. Output unchanged.
          */
         
         fprintf(outfile, "%s\n", buffer);
         
      }
         
   }

   fclose(newsrc);
   fclose(outfile);
   return(1);

}
struct newsgroup *retrieveGroup(char *ngName) {

   struct newsgroup *ngPointer = ngHead;
   
   while(ngPointer) {
      if (!strcmp(ngPointer->name, ngName)) {
         return(ngPointer);
      }
      ngPointer = ngPointer->next;
   }
   return(NULL);

}

struct newsgroup *insertGroup(char *ngName) {
   
   struct newsgroup *ngPointer = 
                    (struct newsgroup *) malloc(sizeof(struct newsgroup));
   ngPointer->next = NULL;
   ngPointer->min = LONG_MAX;
   ngPointer->max = 0;
   ngPointer->firstRange = NULL;
   ngPointer->lastRange = NULL;
   ngPointer->name = strdup(ngName);
   if (ngTail) {
      ngTail->next = ngPointer;
   } else {
      ngHead = ngPointer;
   }
   ngTail = ngPointer;

   return(ngPointer);

}

void appendRange(struct newsgroup *group, struct range *range) {

   if (group->min > range->from) group->min = range->from;
   if (group->max < range->to) group->max = range->to;
   range->next = NULL;
   if (group->lastRange) {
      group->lastRange->next = range;
   } else {
      group->firstRange = range;
   }
   group->lastRange = range;
      
}

void insertRange(struct newsgroup *group, long int article) {

   struct range *newRange, *currentRange;
   struct range **lastRange;

#ifdef DEBUG
   printf("insertRange group %s, article %ld\n", group->name, article);
   for (currentRange = group->firstRange; currentRange != 0; 
        currentRange = currentRange->next) 
      printf("%ld-%ld ", currentRange->from, currentRange->to);
   printf("\n");
#endif

   if (group->min > article) {

#ifdef DEBUG
   printf("article less than minimum of %ld, ", group->min);
#endif

      group->min = article;
      if (group->firstRange && group->firstRange->from == article+1) {

#ifdef DEBUG
   printf("extending first range\n");
#endif

         group->firstRange->from = article;
      } else {   

#ifdef DEBUG
   printf("prepending new range\n");
#endif

         newRange = (struct range *) malloc(sizeof(struct range));
         newRange->next = group->firstRange;
         newRange->from = article; 
         newRange->to = article; 
         group->firstRange = newRange;
         if (!group->lastRange) {
            group->lastRange = newRange;
            group->max = article;
         }
      }
      return;
   }
   if (group->max < article) {

#ifdef DEBUG
   printf("article greater than maximum of %ld, ", group->max);
#endif

      group->max = article;
      if (group->lastRange && group->lastRange->to == article-1) {

#ifdef DEBUG
   printf("extending last range\n");
#endif

         group->lastRange->to = article;
      } else {   

#ifdef DEBUG
   printf("appending new range\n");
#endif

         newRange = (struct range *) malloc(sizeof(struct range));
         newRange->next = NULL;
         newRange->from = article; 
         newRange->to = article; 
         if (group->lastRange) {
            group->lastRange->next = newRange;
            group->lastRange = newRange;
         } else {
            group->lastRange = newRange;
         }
         if (!group->firstRange) {
            group->firstRange = newRange;
            group->min = article;
         }
      }
      return;
   }

#ifdef DEBUG
   printf("article inside group range, ");
#endif

   currentRange = group->firstRange;
   lastRange = &(group->firstRange);

   while (currentRange) {
      if ((article >= currentRange->from) && (article <= currentRange->to)) {

#ifdef DEBUG
   printf("already covered by %ld-%ld\n", currentRange->from, currentRange->to);
#endif

         article = 0;
         break;
      } else if (article == currentRange->from-1) {

#ifdef DEBUG
   printf("extending range %ld-%ld\n", currentRange->from, currentRange->to);
#endif

         currentRange->from --;
         article = 0;
         break;
      } else if (article < currentRange->from) {

#ifdef DEBUG
   printf("inserting single range\n");
#endif

         newRange = (struct range *) malloc(sizeof(struct range));
         newRange->next = currentRange;
         newRange->from = article; 
         newRange->to = article; 
         *lastRange = newRange;
         article = 0;
         break;
      } else if (article == currentRange->to+1) {

#ifdef DEBUG
   printf("extending range %ld-%ld\n", currentRange->from, currentRange->to);
#endif

         currentRange->to ++;
         if ((currentRange->next) && (currentRange->next->from == article+1)) {
            struct range *tempRange;

#ifdef DEBUG
   printf("(merging ranges)\n");
#endif

            currentRange->to = currentRange->next->to;
            tempRange = currentRange->next;
            currentRange->next = currentRange->next->next;
            if (group->lastRange == tempRange) {
               group->lastRange = currentRange->next;
               if(!group->lastRange)  group->lastRange = currentRange;
            }
            free(tempRange);
         }
         article = 0;
         break;
      }
      lastRange = &(currentRange->next);
      currentRange = currentRange->next;
   }

   if (article) {
      newRange = (struct range *) malloc(sizeof(struct range));
      newRange->next = NULL;
      newRange->from = article; 
      newRange->to = article; 
      group->lastRange = newRange;
   }
      
}
            
void parseHistory(char *filename) {

   FILE *history;
   char buffer[LINELENGTH];
   char *id, *dummy, *group, *num;
   struct newsgroup *ngCurrent = ngHead;

   history = fopen(filename, "r");
   if (!history) return;

   while (!feof(history)) {

      fgets(buffer, sizeof(buffer)-1, history);

      if ((id = strtok(buffer, " \t")) &&
          (dummy = strtok(NULL, " \t"))) {

         while ((group = strtok(NULL, "/")) &&
          (num = strtok(NULL, " "))) {

         if (mergeMode && !isIdRead(id)) continue;
            
         if (!ngCurrent) {
            ngCurrent = retrieveGroup(group);
         } else if (strcmp(ngCurrent->name, group)) {
            ngCurrent = retrieveGroup(group);
         }

         if (ngCurrent) {
            int articleRead = 0;
            long int articleNumber = atol(num);

            if (mergeMode) {
               insertRange(ngCurrent, articleNumber);
               continue;
            }
   
            if ((articleNumber >= ngCurrent->min) &&
               (articleNumber <= ngCurrent->max)) {
               struct range* thisRange = ngCurrent->firstRange;
               while (thisRange) {
                  if ((articleNumber >= thisRange->from) && 
                      (articleNumber <= thisRange->to)) {
                     articleRead = 1;
                     break;
                  }
                  if (articleNumber < thisRange->to) break;
                  thisRange = thisRange->next;
               }
            }
            if (articleRead) {
               printf("%s\n", id);
            }
         }}
      }

   }
   fclose(history);
   
}

int compareStrings(const void *a, const void *b) {
   return(strcmp(*((char **) a), *((char **) b)));
}

int readIDs() {

   char buffer[LINELENGTH];
   int size=0, i=0;

   while(!feof(stdin)) {
   
      fgets(buffer, sizeof(buffer)-1, stdin);
      if (count >= size) {
         char **oldList = idList;
         idList = malloc((size+200) * sizeof(char *));
         for (i=0; i<size; i++) idList[i]=oldList[i];
         size+=200;
         if (oldList) free(oldList);   
      }
      if (buffer[strlen(buffer)-1] == '\n') buffer[strlen(buffer)-1] = 0;
      idList[count++] = strdup(buffer);
   
   }

   // for(i=0; i<count;i++) printf("%s\n", idList[i])
   qsort(idList, count, sizeof(char *), compareStrings);

   return(1);

}

int isIdRead(char *msgId) {

   int returnValue = (bsearch((const void *) &msgId, idList, count, sizeof(char *), compareStrings) != NULL);

#ifdef DEBUG
   printf("\nisIdRead(%s) = %d\n", msgId, returnValue);
#endif

   return(returnValue);

}
