/* Tool to apply a Nodediff upon a nodelist.
   Written 1999 by Tobias Ernst and released to the Public Domain.
   References: FTS-0005
*/

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <stdlib.h>
#include "crc16.h"
#include "version.h"

/* It is no problem if a line grows larger than BUFSZ. This code is
   intelligent. :-)
 */
#define BUFSZ 128

unsigned short actualcrc;


/* A state machine to analyse the very first line of a nodelist for day
   number and an optional CRC value. */

enum {SCANDASH, SCANFDIGIT, DAYVAL, SCANCOLON, SCANNDIGIT, CRCVAL, AFTERCRCVAL, FINISH};

int  analyze_first_line(FILE *f, unsigned short *crcnum, int *has_crc,
                        unsigned short *daynum)
{
    char c=0;
    int state = SCANDASH;
    unsigned short crc, day, hcrc, result = 1;

    while (state != FINISH)
    {
        if (fread(&c, 1, 1, f) != 1)
        {
            result = 0;
            state = FINISH;
            continue;
        }

        if ( c == '\n' )
        {
            hcrc = 1;
            if (state < CRCVAL)
            {
                hcrc = 0;
            }
            if (state < DAYVAL)
            {
                result = 0;     /* analysis failed */
            }
            state = FINISH;
        }
        else if (isdigit(c))
        {
            switch (state)
            {
            case SCANFDIGIT:
                day = c - '0';
                state = DAYVAL;
                break;
            case SCANNDIGIT:
                crc = c - '0';
                state = CRCVAL;
                break;
            case DAYVAL:
                day = day * 10U + (c - '0');
                break;
            case CRCVAL:
                crc = crc * 10U + (c - '0');
                break;
            default:;
            }
        }
        else if (c == '-')
        {
            switch (state)
            {
            case SCANDASH:
                state = SCANFDIGIT;
                break;
            default:;
            }
        }
        else if (c == ':')
        {
            switch (state)
            {
            case SCANCOLON:
                state = SCANNDIGIT;
                break;
            default:;
            }
        }
        else
        {
            switch(state)
            {
            case CRCVAL:
                state = AFTERCRCVAL;
                break;
            case DAYVAL:
                state = SCANCOLON;
                break;
            default:;
            }
        }
    }

    *crcnum = crc; *daynum = day; *has_crc = hcrc;
    return result;
}


/* Copy a line from in to out, no matter how long it is, and register it
   with the CRC checker. */

int passline (FILE *from, FILE *to)
{
    char buf1[BUFSZ];
    size_t l;

    do
    {
        if (fgets(buf1, BUFSZ, from) == NULL)
            return -1;
        if (fputs(buf1, to) == EOF)
            return -1;
        l = strlen(buf1);
        crc16_process(&actualcrc, (unsigned char *)buf1, l);
        if (!l)
            return -1;
    } while (buf1[l - 1] != '\n');
    return 0;
}

/* Skip a line. */

int skipline (FILE *from)
{
    char buf1[BUFSZ];
    size_t l;

    do
    {
        if (fgets(buf1, BUFSZ, from) == NULL)
            return -1;
        l = strlen(buf1);
        if (!l)
            return -1;
    } while (buf1[l - 1] != '\n');
    return 0;
}

/* Compare two lines. */

int compareline (FILE *from1, FILE *from2)
{
    char buf1[BUFSZ], buf2[BUFSZ];
    size_t l;

    do
    {
        if (fgets(buf1, BUFSZ, from1) == NULL)
            return -1;
        if (fgets(buf2, BUFSZ, from2) == NULL)
            return -1;
        l = strlen(buf1);
        if (!l)
            return -1;
        if (memcmp(buf1, buf2, l))
            return -2;
    } while (buf1[l - 1] != '\n');
    return 0;
}

/* Analyse a Nodediff command */

enum {COPY, ADD, DELETE, END, ILL};

int readcommand(FILE *f, int *argument)
{
    char buffer[64];
    int cmd, arg;

    if (fgets(buffer, 64, f) == NULL)
        return END;

    if (*buffer == '\0' || *buffer == 0x1A)
        return END;

    switch (*buffer)
    {
    case 'A':
        cmd = ADD;
        break;
    case 'C':
        cmd = COPY;
        break;
    case 'D':
        cmd = DELETE;
        break;
    default:
        return ILL;
    }

    arg = atoi(buffer+1);
    if (arg < 1)
        return ILL;

    *argument = arg;
    return cmd;
}

/* Construct the name of the new nodelist from the old name plus the
   extension of the nodediff file */

char *construct_new_filename(char *listname, char *diffname)
{
    int l, m;
    char *tempname;

    /* Sanity checks on the given filenames */
    if ((l = strlen(listname)) < 5)
    {
        fprintf (stderr, "%s is not a valid nodelist filename.\n", listname);
        return NULL;
    }
    if (listname[l-4] != '.' || !isdigit(listname[l-3]) ||
        !isdigit(listname[l-2]) || !isdigit(listname[l-1]))
    {
        fprintf (stderr, "%s is not a valid nodelist filename.\n", listname);
        return NULL;
    }
    if ((m = strlen(diffname)) < 5)
    {
        fprintf (stderr, "%s is not a valid nodediff filename.\n", listname);
        return NULL;
    }
    if (diffname[m-4] != '.' || !isdigit(diffname[m-3]) ||
        !isdigit(diffname[m-2]) || !isdigit(diffname[m-1]))
    {
        fprintf (stderr, "%s is not a valid nodediff filename.\n", listname);
        return NULL;
    }

    /* test for misuse like "nldiff -n nodelist.365 nodediff.365 */
    if (!strcmp(diffname+m-4, listname+l-4))
    {
        fprintf (stderr, "%s does not seem to apply to %s.\n",
                 diffname, listname);
        return NULL;
    }

    /* Construct filename of new nodelist */
    if ((tempname = malloc(strlen(listname) + 1)) == NULL)
    {
        fprintf (stderr, "Out of memory.\n");
        return NULL;
    }
    strcpy(tempname, listname);
    strcpy(tempname+l-4, diffname+m-4);

    return tempname;
}

/* Parse command line arguments */

enum { REMOVE_NODELIST = 1, REMOVE_NODEDIFF = 2 };

int parse_args (int argc, char **argv, char **listname, char **diffname,
                int *pflags)
{
    char *args[2];
    int i,j = 0;
    int flags;

    flags = 0;

    for (i = 1; i < argc; i++)
    {
        if (argv[i][0] == '-')
        {
            switch (argv[i][1])
            {
            case 'n':
                flags = flags | REMOVE_NODELIST;
                break;
            case 'd':
                flags = flags | REMOVE_NODEDIFF;
                break;
            default:
                return 0;
            }
            if (argv[i][2])
            {
                return 0;
            }
            continue;
        }

        if (j<2)
        {
            args[j++] = argv[i];
        }
    }

    if (j == 2)
    {
        *listname = args[0];
        *diffname = args[1];
        *pflags = flags;
        return 1;
    }
    return 0;
}

/* Usage help */

void usage(void)
{
    fprintf (stderr, "nldiff - nodelist differ rev. %s\n", REV);
    fprintf (stderr, "Usage:\n"\
"   nldiff [-n] [-d] LISTNAME.DNR DIFFNAME.DNR\n\n"\
"   The -n option causes the original Nodelist to be deleted if the new\n"\
"   nodelist could be successfully generated.\n"\
"   The -d option causes the Nodediff file to be deleted if the new\n"\
"   nodelist could be successfully generated.\n\n"\
"Example:\n"\
"   nldiff -n NODELIST.253 NODEDIFF.260\n\n"\
"Remarks:\n"\
"   If you want a tool that automatically updates your nodelist\n"\
"   without you having to manually specify the day number file\n"\
"   extensions, use \"nlupdate\". It will call nldiff internally.\n");
}

/* Main program */

int main(int argc, char **argv)
{
    FILE *fn = NULL, *fd = NULL, *fo = NULL;
    char *listname = NULL, *diffname = NULL, *tempname = NULL;
    int cmd, arg, i, hascrc, rv;
    unsigned short crc, newday, expnewday;
    int crci = 0;
    int flags = 0;

    /* parse the command line */
    if (!parse_args(argc, argv, &listname, &diffname, &flags))
    {
        usage();
        return 8;
    }

    /* construct the filename of the new nodelist */
    if ((tempname = construct_new_filename(listname, diffname)) == NULL)
        return 8;
    expnewday=atoi(tempname + strlen(tempname) - 3);

    /* open the files */
    if ((fn = fopen(listname, "rb")) == NULL)
    {
        fprintf (stderr, "Cannot open %s.\n", listname); goto abnormal;
    }
    if ((fd = fopen(diffname, "rb")) == NULL)
    {
        fprintf (stderr, "Cannot open %s.\n", diffname); goto abnormal;
    }
    if ((fo = fopen(tempname, "w+b")) == NULL)
    {
        fprintf (stderr, "Cannot create %s.\n", tempname); goto abnormal;
    }

    /* Test if the diff's first line matches the list's first line */
    switch (compareline(fn, fd))
    {
    case -2:
        fprintf (stderr, "%s does not seem to apply to %s.\n",
                 diffname, listname);
        goto abnormal;
    case -1:
        fprintf (stderr, "I/O error.\n");
        goto abnormal;
    default:;                   /* match! */
    }
    if (fseek(fn, 0L, SEEK_SET))
    {
        fprintf (stderr, "File seek error.\n");
        goto abnormal;
    }

    /* Interpret the commands in the Diff file */
    do
    {
        cmd = readcommand(fd, &arg);
        switch(cmd)
        {
        case COPY:
            for (i = 0; i < arg; i++)
            {
                if (passline(fn, fo))
                {
                    fprintf (stderr, "Copy failed (%d / %d)\n", i, arg);
                    goto abnormal;
                }
                if (!crci)
                {
                    crc16_init(&actualcrc);
                    crci = 1;
                }
            }

            break;
        case ADD:
            for (i = 0; i < arg; i++)
            {
                if (passline(fd, fo))
                {
                    fprintf (stderr, "Add failed (%d / %d)\n", i, arg);
                    goto abnormal;
                }
                if (!crci)
                {
                    crc16_init(&actualcrc);
                    crci = 1;
                }
            }
            break;
        case DELETE:
            for (i = 0; i < arg; i++)
                if (skipline(fn))
                {
                    fprintf (stderr, "Delete failed (%d / %d)\n", i, arg);
                    goto abnormal;
                }
            break;
        case END:
            break;
        default:
            fprintf (stderr, "Illegal command encountered.\n");
            goto abnormal;
        }
    } while (cmd != END);
    fputc(0x1A, fo);

    /* Now determine the actual day number and the expected CRC of the newly
       written nodelist file */
    if ((fseek(fo, 0L, SEEK_SET)) ||
        (!analyze_first_line(fo, &crc, &hascrc, &newday)))
    {
        fprintf (stderr, "New file is not a valid nodelist.\n");
        goto abnormal;
    }

    /* Compare the CRC values */
    crc16_finalize(&actualcrc);
    if (hascrc && actualcrc != crc)
    {
        fprintf (stderr, "New file does not pass CRC test.\n");
        goto abnormal;
    }
    if (newday != expnewday)
    {
        fprintf (stderr, "New day number and diff file name do not match.\n");
        goto abnormal;
    }

    fclose(fn); fclose(fd); fclose(fo); rv=0;

    /* Delete files that are not needed any more, if the user requested it. */
    if ((flags & REMOVE_NODELIST) && remove(listname))
    {
        fprintf (stderr, "Cannot remove old nodelist file %s.\n", listname);
        rv = 8;
    }
    if ((flags & REMOVE_NODEDIFF) && remove(diffname))
    {
        fprintf (stderr, "Cannot remove old nodediff file %s.\n", diffname);
        rv = 8;
    }

    return rv;

abnormal:
    fprintf (stderr, "Processing aborted.\n");
    if (fn != NULL)
        fclose(fn);
    if (fd != NULL)
        fclose(fd);
    if (fo != NULL)
        fclose(fo);
    if (tempname != NULL)
    {
        if (fo != NULL) remove(tempname);
        free(tempname);
    }
    return 8;
}



