/*
**  Shrink files on line boundaries.
**
**  Written by Landon Curt Noll <chongo@toad.com>, and placed in the
**  public domain.  Rewritten for INN by Rich Salz.
**
**  Usage:
**      shrinkfile [-nv] [-m maxsize] [-s size] file [file ...]
**      -n              No writes, exit 0 if any file is too large, 1 otherwise
**      -s size         Truncation size (0 default); suffix may be k, m,
**                      or g to scale.  Must not be larger than 2^31 - 1.
**      -m maxsize      Maximum size allowed before truncation.  If maxsize
**                      <= size, then it is reset to size.  Default == size.
**      -v              Print status line.
**
**  Files will be shrunk at end of line boundary.  In no case will the
**  file be longer than size bytes if it was longer than maxsize bytes.
**  If the first line is longer than the absolute value of size, the file
**  will be truncated to zero length.
**
**  The -n flag may be used to determine if any file is too large.  No
**  files will be altered in this mode.
*/

#include "portable/system.h"

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>

#include "inn/innconf.h"
#include "inn/libinn.h"
#include "inn/messages.h"

#define MAX_SIZE 0x7fffffffUL

static void Usage(void) __attribute__((__noreturn__));


/*
**  Open a safe unique temporary file that will go away when closed.
*/
static FILE *
OpenTemp(void)
{
    FILE *F;
    char *filename;
    int fd;

    filename = concatpath(innconf->pathtmp, "shrinkXXXXXX");
    fd = mkstemp(filename);
    if (fd < 0)
        sysdie("cannot create temporary file");
    F = fdopen(fd, "w+");
    if (F == NULL)
        sysdie("cannot fdopen %s", filename);
    unlink(filename);
    free(filename);
    return F;
}


/*
**  Does file end with \n?  Assume it does on I/O error, to avoid doing I/O.
*/
static int
EndsWithNewline(FILE *F)
{
    int c;

    if (fseeko(F, 1, SEEK_END) < 0) {
        syswarn("cannot seek to end of file");
        return true;
    }

    /* return the actual character or EOF */
    if ((c = fgetc(F)) == EOF) {
        if (ferror(F))
            syswarn("cannot read last byte");
        return true;
    }
    return c == '\n';
}


/*
**  Add a newline to location of a file.
*/
static bool
AppendNewline(char *name)
{
    FILE *F;

    if ((F = xfopena(name)) == NULL) {
        syswarn("cannot add newline");
        return false;
    }

    if (fputc('\n', F) == EOF || fflush(F) == EOF || ferror(F)
        || fclose(F) == EOF) {
        syswarn("cannot add newline");
        return false;
    }

    return true;
}

/*
**  Just check if it is too big.
*/
static bool
TooBig(FILE *F, off_t maxsize)
{
    struct stat Sb;

    /* Get the file's size. */
    if (fstat((int) fileno(F), &Sb) < 0) {
        syswarn("cannot fstat");
        return false;
    }

    /* return true if too large */
    return (maxsize > Sb.st_size ? false : true);
}

/*
**  This routine does all the work.
*/
static bool
Process(FILE *F, char *name, off_t size, off_t maxsize, bool *Changedp)
{
    off_t len;
    FILE *tmp;
    struct stat Sb;
    char buff[BUFSIZ + 1];
    int c;
    size_t i;
    bool err;

    /* Get the file's size. */
    if (fstat((int) fileno(F), &Sb) < 0) {
        syswarn("cannot fstat");
        return false;
    }
    len = Sb.st_size;

    /* Process a zero size request. */
    if (size == 0 && len > maxsize) {
        if (len > 0) {
            fclose(F);
            if ((F = fopen(name, "w")) == NULL) {
                syswarn("cannot overwrite");
                return false;
            }
            fclose(F);
            *Changedp = true;
        }
        return true;
    }

    /* See if already small enough. */
    if (len <= maxsize) {
        /* Newline already present? */
        if (EndsWithNewline(F)) {
            fclose(F);
            return true;
        }

        /* No newline, add it if it fits. */
        if (len < size - 1) {
            fclose(F);
            *Changedp = true;
            return AppendNewline(name);
        }
    } else if (!EndsWithNewline(F)) {
        if (!AppendNewline(name)) {
            fclose(F);
            return false;
        }
    }

    /* We now have a file that ends with a newline that is bigger than
     * we want.  Starting from {size} bytes from end, move forward
     * until we get a newline. */
    if (fseeko(F, -size, SEEK_END) < 0) {
        syswarn("cannot fseeko");
        fclose(F);
        return false;
    }

    while ((c = getc(F)) != '\n')
        if (c == EOF) {
            syswarn("cannot read");
            fclose(F);
            return false;
        }

    /* Copy rest of file to temp. */
    tmp = OpenTemp();
    err = false;
    while ((i = fread(buff, 1, sizeof buff, F)) > 0)
        if (fwrite(buff, 1, i, tmp) != i) {
            err = true;
            break;
        }
    if (err) {
        syswarn("cannot copy to temporary file");
        fclose(F);
        fclose(tmp);
        return false;
    }

    /* Now copy temp back to original file. */
    fclose(F);
    if ((F = fopen(name, "w")) == NULL) {
        syswarn("cannot overwrite file");
        fclose(tmp);
        return false;
    }
    fseeko(tmp, 0, SEEK_SET);

    while ((i = fread(buff, 1, sizeof buff, tmp)) > 0)
        if (fwrite(buff, 1, i, F) != i) {
            err = true;
            break;
        }
    if (err) {
        syswarn("cannot overwrite file");
        fclose(F);
        fclose(tmp);
        return false;
    }

    fclose(F);
    fclose(tmp);
    *Changedp = true;
    return true;
}


/*
**  Convert size argument to numeric value.  Return -1 on error.
*/
static off_t
ParseSize(char *p)
{
    off_t scale;
    unsigned long str_num;
    char *q;

    /* Skip leading spaces */
    while (ISWHITE(*p))
        p++;
    if (*p == '\0')
        return -1;

    /* determine the scaling factor */
    q = &p[strlen(p) - 1];
    switch (*q) {
    default:
        return -1;
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        scale = 1;
        break;
    case 'k':
    case 'K':
        scale = 1024;
        *q = '\0';
        break;
    case 'm':
    case 'M':
        scale = 1024 * 1024;
        *q = '\0';
        break;
    case 'g':
    case 'G':
        scale = 1024 * 1024 * 1024;
        *q = '\0';
        break;
    }

    /* Convert string to number. */
    if (sscanf(p, "%lud", &str_num) != 1)
        return -1;
    if (str_num > MAX_SIZE / scale)
        die("size is too big");

    return scale * str_num;
}


/*
**  Print usage message and exit.
*/
static void
Usage(void)
{
    fprintf(
        stderr,
        "Usage: shrinkfile [-nv] [-m maxsize] [-s size] file [file ...]\n");
    exit(1);
}


int
main(int ac, char *av[])
{
    bool Changed;
    bool Verbose;
    bool no_op;
    FILE *F;
    char *p;
    int i;
    off_t size = 0;
    off_t maxsize = 0;

    /* First thing, set up our identity. */
    message_program_name = "shrinkfile";

    /* Set defaults. */
    Verbose = false;
    no_op = false;
    umask(NEWSUMASK);

    if (!innconf_read(NULL))
        exit(1);

    /* Parse JCL. */
    while ((i = getopt(ac, av, "m:s:vn")) != EOF)
        switch (i) {
        default:
            Usage();
            /* NOTREACHED */
        case 'n':
            no_op = true;
            break;
        case 'm':
            if ((maxsize = ParseSize(optarg)) < 0)
                Usage();
            break;
        case 's':
            if ((size = ParseSize(optarg)) < 0)
                Usage();
            break;
        case 'v':
            Verbose = true;
            break;
        }
    if (maxsize < size) {
        maxsize = size;
    }
    ac -= optind;
    av += optind;
    if (ac == 0)
        Usage();

    while ((p = *av++) != NULL) {
        if ((F = fopen(p, "r")) == NULL) {
            syswarn("cannot open %s", p);
            continue;
        }

        /* If -n (no_op). */
        if (no_op) {

            /* Check if too big and exit zero if it is. */
            if (TooBig(F, maxsize)) {
                if (Verbose)
                    notice("%s is too large", p);
                exit(0);
                /* NOTREACHED */
            }
        } else {
            /* No -n, do some real work. */
            Changed = false;
            if (!Process(F, p, size, maxsize, &Changed))
                syswarn("cannot shrink %s", p);
            else if (Verbose && Changed)
                notice("shrunk %s", p);
        }
    }
    if (no_op && Verbose) {
        notice("did not find a file that was too large");
    }

    /* If -n, then exit non-zero to indicate no file too big. */
    exit(no_op ? 1 : 0);
    /* NOTREACHED */
}
