/* $Id: md5sum.c 2984 2016-11-01 18:24:11Z bird $ */
/** @file
 * md5sum.
 */

/*
 * Copyright (c) 2007-2010 knut st. osmundsen <bird-kBuild-spamx@anduin.net>
 *
 * This file is part of kBuild.
 *
 * kBuild is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * kBuild is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with kBuild.  If not, see <http://www.gnu.org/licenses/>
 *
 */

/*******************************************************************************
*   Header Files                                                               *
*******************************************************************************/
#include "config.h"
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#ifdef _MSC_VER
# include <io.h>
#else
# include <unistd.h>
#endif
#include <sys/stat.h>
#include "err.h"
#include "kmkbuiltin.h"
#include "../../lib/md5.h"

/*#define MD5SUM_USE_STDIO*/


/**
 * Prints the usage and return 1.
 */
static int usage(FILE *pOut)
{
    fprintf(pOut,
            "usage: md5sum [-bt] [-o list-file] file(s)\n"
            "   or: md5sum [-btwq] -c list-file(s)\n"
            "   or: md5sum [-btq] -C MD5 file\n"
            "\n"
            " -c, --check       Check MD5 and files found in the specified list file(s).\n"
            "                   The default is to compute MD5 sums of the specified files\n"
            "                   and print them to stdout in list form.\n"
            " -C, --check-file  This is followed by an MD5 sum and the file to check.\n"
            " -b, --binary      Read files in binary mode. (default)\n"
            " -t, --text        Read files in text mode.\n"
            " -p, --progress    Show progress indicator on large files.\n"
            " -o, --output      Name of the output list file. Useful with -p.\n"
            " -q, --status      Be quiet.\n"
            " -w, --warn        Ignored. Always warn, unless quiet.\n"
            " -h, --help        This usage info.\n"
            " -v, --version     Show version information and exit.\n"
            );
    return 1;
}


/**
 * Makes a string out of the given digest.
 *
 * @param   pDigest     The MD5 digest.
 * @param   pszDigest   Where to put the digest string. Must be able to
 *                      hold at least 33 bytes.
 */
static void digest_to_string(unsigned char pDigest[16], char *pszDigest)
{
    unsigned i;
    for (i = 0; i < 16; i++)
    {
        static char s_achDigits[17] = "0123456789abcdef";
        pszDigest[i*2]     = s_achDigits[(pDigest[i] >> 4)];
        pszDigest[i*2 + 1] = s_achDigits[(pDigest[i] & 0xf)];
    }
    pszDigest[i*2] = '\0';
}


/**
 * Attempts to convert a string to a MD5 digest.
 *
 * @returns 0 on success, 1-based position of the failure first error.
 * @param   pszDigest   The string to interpret.
 * @param   pDigest     Where to put the MD5 digest.
 */
static int string_to_digest(const char *pszDigest, unsigned char pDigest[16])
{
    unsigned i;
    unsigned iBase = 1;

    /* skip blanks */
    while (     *pszDigest == ' '
           ||   *pszDigest == '\t'
           ||   *pszDigest == '\n'
           ||   *pszDigest == '\r')
        pszDigest++, iBase++;

    /* convert the digits. */
    memset(pDigest, 0, 16);
    for (i = 0; i < 32; i++, pszDigest++)
    {
        int iDigit;
        if (*pszDigest >= '0' && *pszDigest <= '9')
            iDigit = *pszDigest - '0';
        else if (*pszDigest >= 'a' && *pszDigest <= 'f')
            iDigit = *pszDigest - 'a' + 10;
        else if (*pszDigest >= 'A' && *pszDigest <= 'F')
            iDigit = *pszDigest - 'A' + 10;
        else
            return i + iBase;
        if (i & 1)
            pDigest[i >> 1] |= iDigit;
        else
            pDigest[i >> 1] |= iDigit << 4;
    }

    /* the rest of the string must now be blanks. */
    while (     *pszDigest == ' '
           ||   *pszDigest == '\t'
           ||   *pszDigest == '\n'
           ||   *pszDigest == '\r')
        pszDigest++, i++;

    return *pszDigest ? i + iBase : 0;
}


/**
 * Opens the specified file for md5 sum calculation.
 *
 * @returns Opaque pointer on success, NULL and errno on failure.
 * @param   pszFilename     The filename.
 * @param   fText           Whether text or binary mode should be used.
 */
static void *open_file(const char *pszFilename, unsigned fText)
{
#if defined(MD5SUM_USE_STDIO)
    FILE *pFile;

    errno = 0;
    pFile = fopen(pszFilename, fText ? "r" : "rb");
    if (!pFile && errno == EINVAL && !fText)
        pFile = fopen(pszFilename, "r");
    return pFile;

#else
    int fd;
    int fFlags;

    /* figure out the appropriate flags. */
    fFlags = O_RDONLY;
#ifdef O_SEQUENTIAL
    fFlags |= _O_SEQUENTIAL;
#elif defined(_O_SEQUENTIAL)
    fFlags |= _O_SEQUENTIAL;
#endif
#ifdef O_BINARY
    if (!fText)     fFlags |= O_BINARY;
#elif defined(_O_BINARY)
    if (!fText)     fFlags |= _O_BINARY;
#endif
#ifdef O_TEXT
    if (fText)      fFlags |= O_TEXT;
#elif defined(O_TEXT)
    if (fText)      fFlags |= _O_TEXT;
#else
    (void)fText;
#endif

    errno = 0;
    fd = open(pszFilename, fFlags, 0755);
    if (fd >= 0)
    {
        int *pFd = malloc(sizeof(*pFd));
        if (pFd)
        {
            *pFd = fd;
            return pFd;
        }
        close(fd);
        errno = ENOMEM;
    }

    return NULL;
#endif
}


/**
 * Closes a file opened by open_file.
 *
 * @param   pvFile          The opaque pointer returned by open_file.
 */
static void close_file(void *pvFile)
{
#if defined(MD5SUM_USE_STDIO)
    fclose((FILE *)pvFile);
#else
    close(*(int *)pvFile);
    free(pvFile);
#endif
}


/**
 * Reads from a file opened by open_file.
 *
 * @returns Number of bytes read on success.
 *          0 on EOF.
 *          Negated errno on read error.
 * @param   pvFile          The opaque pointer returned by open_file.
 * @param   pvBuf           Where to put the number of read bytes.
 * @param   cbBuf           The max number of bytes to read.
 *                          Must be less than a INT_MAX.
 */
static int read_file(void *pvFile, void *pvBuf, size_t cbBuf)
{
#if defined(MD5SUM_USE_STDIO)
    int cb;

    errno = 0;
    cb = (int)fread(pvBuf, 1, cbBuf, (FILE *)pvFile);
    if (cb >= 0)
        return (int)cb;
    if (!errno)
        return -EINVAL;
    return -errno;
#else
    int cb;

    errno = 0;
    cb = (int)read(*(int *)pvFile, pvBuf, (int)cbBuf);
    if (cb >= 0)
        return (int)cb;
    if (!errno)
        return -EINVAL;
    return -errno;
#endif
}


/**
 * Gets the size of the file.
 * This is informational and not necessarily 100% accurate.
 *
 * @returns File size.
 * @param   pvFile          The opaque pointer returned by open_file
 */
static double size_file(void *pvFile)
{
#if defined(_MSC_VER)
    __int64 cb;
# if defined(MD5SUM_USE_STDIO)
    cb = _filelengthi64(fileno((FILE *)pvFile));
# else
    cb = _filelengthi64(*(int *)pvFile);
# endif
    if (cb >= 0)
        return (double)cb;

#elif defined(MD5SUM_USE_STDIO)
    struct stat st;
    if (!fstat(fileno((FILE *)pvFile), &st))
        return st.st_size;

#else
    struct stat st;
    if (!fstat(*(int *)pvFile, &st))
        return st.st_size;
#endif
    return 1024;
}


/**
 * Calculates the md5sum of the sepecified file stream.
 *
 * @returns errno on failure, 0 on success.
 * @param   pvFile      The file stream.
 * @param   pDigest     Where to store the MD5 digest.
 * @param   fProgress   Whether to show a progress bar.
 */
static int calc_md5sum(void *pvFile, unsigned char pDigest[16], unsigned fProgress)
{
    int cb;
    int rc = 0;
    struct MD5Context Ctx;
    unsigned uPercent = 0;
    double off = 0.0;
    double cbFile = size_file(pvFile);

    /* Get a decent sized buffer assuming we'll be spending more time reading
       from the storage than doing MD5 sums.  (2MB was choosen based on recent
       SATA storage benchmarks which used that block size for sequential
       tests.) We align the buffer address on a 16K boundrary to avoid most
       transfer alignment issues. */
    char        *pabBufAligned;
    size_t const cbBufAlign = 16*1024 - 1;
    size_t const cbBufMax = 2048*1024;
    size_t       cbBuf    = cbFile >= cbBufMax ? cbBufMax : ((size_t)cbFile + cbBufAlign) & ~(size_t)cbBufAlign;
    char        *pabBuf   = (char *)malloc(cbBuf + cbBufAlign);
    if (pabBuf)
        pabBufAligned = (char *)(((uintptr_t)pabBuf + cbBufAlign) & ~(uintptr_t)cbBufAlign );
    else
    {
        do
        {
            cbBuf /= 2;
            pabBuf = (char *)malloc(cbBuf);
        } while (!pabBuf && cbBuf > 4096);
        if (!pabBuf)
            return ENOMEM;
        pabBufAligned = pabBuf;
    }

    if (cbFile < cbBuf * 4)
        fProgress = 0;

    MD5Init(&Ctx);
    for (;;)
    {
        /* process a chunk. */
        cb = read_file(pvFile, pabBufAligned, cbBuf);
        if (cb > 0)
            MD5Update(&Ctx, (unsigned char *)pabBufAligned, cb);
        else if (!cb)
            break;
        else
        {
            rc = -cb;
            break;
        }

        /* update the progress indicator. */
        if (fProgress)
        {
            unsigned uNewPercent;
            off += cb;
            uNewPercent = (unsigned)((off / cbFile) * 100);
            if (uNewPercent != uPercent)
            {
                if (uPercent)
                    printf("\b\b\b\b");
                printf("%3d%%", uNewPercent);
                fflush(stdout);
                uPercent = uNewPercent;
            }
        }
    }
    MD5Final(pDigest, &Ctx);

    if (fProgress)
        printf("\b\b\b\b    \b\b\b\b");

    free(pabBuf);
    return rc;
}


/**
 * Checks the if the specified digest matches the digest of the file stream.
 *
 * @returns 0 on match, -1 on mismatch, errno value (positive) on failure.
 * @param   pvFile      The file stream.
 * @param   Digest      The MD5 digest.
 * @param   fProgress   Whether to show an progress indicator on large files.
 */
static int check_md5sum(void *pvFile, unsigned char Digest[16], unsigned fProgress)
{
    unsigned char DigestFile[16];
    int rc;

    rc = calc_md5sum(pvFile, DigestFile, fProgress);
    if (!rc)
        rc = memcmp(Digest, DigestFile, 16) ? -1 : 0;
    return rc;
}


/**
 * Checks if the specified file matches the given MD5 digest.
 *
 * @returns 0 if it matches, 1 if it doesn't or an error occurs.
 * @param   pszFilename The name of the file to check.
 * @param   pszDigest   The MD5 digest string.
 * @param   fText       Whether to open the file in text or binary mode.
 * @param   fQuiet      Whether to go about this in a quiet fashion or not.
 * @param   fProgress   Whether to show an progress indicator on large files.
 */
static int check_one_file(const char *pszFilename, const char *pszDigest, unsigned fText, unsigned fQuiet, unsigned fProgress)
{
    unsigned char Digest[16];
    int rc;

    rc = string_to_digest(pszDigest, Digest);
    if (!rc)
    {
        void *pvFile;

        pvFile = open_file(pszFilename, fText);
        if (pvFile)
        {
            if (!fQuiet)
                fprintf(stdout, "%s: ", pszFilename);
            rc = check_md5sum(pvFile, Digest, fProgress);
            close_file(pvFile);
            if (!fQuiet)
            {
                fprintf(stdout, "%s\n", !rc ? "OK" : rc < 0 ? "FAILURE" : "ERROR");
                fflush(stdout);
                if (rc > 0)
                    errx(1, "Error reading '%s': %s", pszFilename, strerror(rc));
            }
            if (rc)
                rc = 1;
        }
        else
        {
            if (!fQuiet)
                errx(1, "Failed to open '%s': %s", pszFilename, strerror(errno));
            rc = 1;
        }
    }
    else
    {
        errx(1, "Malformed MD5 digest '%s'!", pszDigest);
        errx(1, "                      %*s^", rc - 1, "");
        rc = 1;
    }

    return rc;
}


/**
 * Checks the specified md5.lst file.
 *
 * @returns 0 if all checks out file, 1 if one or more fails or there are read errors.
 * @param   pszFilename     The name of the file.
 * @param   fText           The default mode, text or binary. Only used when fBinaryTextOpt is true.
 * @param   fBinaryTextOpt  Whether a -b or -t option was specified and should be used.
 * @param   fQuiet          Whether to be quiet.
 * @param   fProgress   Whether to show an progress indicator on large files.
 */
static int check_files(const char *pszFilename, int fText, int fBinaryTextOpt, int fQuiet, unsigned fProgress)
{
    int rc = 0;
    FILE *pFile;

    /*
     * Try open the md5.lst file and process it line by line.
     */
    pFile = fopen(pszFilename, "r");
    if (pFile)
    {
        int iLine = 0;
        char szLine[8192];
        while (fgets(szLine, sizeof(szLine), pFile))
        {
            const char *pszDigest;
            int fLineText;
            char *psz;
            int rc2;

            iLine++;
            psz = szLine;

            /* leading blanks */
            while (*psz == ' ' || *psz == '\t' || *psz == '\n')
                psz++;

            /* skip blank or comment lines. */
            if (!*psz || *psz == '#' || *psz == ';' || *psz == '/')
                continue;

            /* remove the trailing newline. */
            rc2 = (int)strlen(psz);
            if (psz[rc2 - 1] == '\n')
                psz[rc2 - (rc2 >= 2 && psz[rc2 - 2] == '\r' ? 2 : 1)] = '\0';

            /* skip to the end of the digest and terminate it. */
            pszDigest = psz;
            while (*psz != ' ' && *psz != '\t' && *psz)
                psz++;
            if (*psz)
            {
                *psz++ = '\0';

                /* blanks */
                while (*psz == ' ' || *psz == '\t' || *psz == '\n')
                    psz++;

                /* check for binary asterix */
                if (*psz != '*')
                    fLineText = fBinaryTextOpt ? fText : 0;
                else
                {
                    fLineText = 0;
                    psz++;
                }
                if (*psz)
                {
                    unsigned char Digest[16];

                    /* the rest is filename. */
                    pszFilename = psz;

                    /*
                     * Do the job.
                     */
                    rc2 = string_to_digest(pszDigest, Digest);
                    if (!rc2)
                    {
                        void *pvFile = open_file(pszFilename, fLineText);
                        if (pvFile)
                        {
                            if (!fQuiet)
                                fprintf(stdout, "%s: ", pszFilename);
                            rc2 = check_md5sum(pvFile, Digest, fProgress);
                            close_file(pvFile);
                            if (!fQuiet)
                            {
                                fprintf(stdout, "%s\n", !rc2 ? "OK" : rc2 < 0 ? "FAILURE" : "ERROR");
                                fflush(stdout);
                                if (rc2 > 0)
                                    errx(1, "Error reading '%s': %s", pszFilename, strerror(rc2));
                            }
                            if (rc2)
                                rc = 1;
                        }
                        else
                        {
                            if (!fQuiet)
                                errx(1, "Failed to open '%s': %s", pszFilename, strerror(errno));
                            rc = 1;
                        }
                    }
                    else if (!fQuiet)
                    {
                        errx(1, "%s (%d): Ignoring malformed digest '%s' (digest)", pszFilename, iLine, pszDigest);
                        errx(1, "%s (%d):                            %*s^", pszFilename, iLine, rc2 - 1, "");
                    }
                }
                else if (!fQuiet)
                    errx(1, "%s (%d): Ignoring malformed line!", pszFilename, iLine);
            }
            else if (!fQuiet)
                errx(1, "%s (%d): Ignoring malformed line!", pszFilename, iLine);
        } /* while more lines */

        fclose(pFile);
    }
    else
    {
        errx(1, "Failed to open '%s': %s", pszFilename, strerror(errno));
        rc = 1;
    }

    return rc;
}


/**
 * Calculates the MD5 sum for one file and prints it.
 *
 * @returns 0 on success, 1 on any kind of failure.
 * @param   pszFilename     The file to process.
 * @param   fText           The mode to open the file in.
 * @param   fQuiet          Whether to be quiet or verbose about errors.
 * @param   fProgress       Whether to show an progress indicator on large files.
 * @param   pOutput         Where to write the list. Progress is always written to stdout.
 */
static int md5sum_file(const char *pszFilename, unsigned fText, unsigned fQuiet, unsigned fProgress, FILE *pOutput)
{
    int rc;
    void *pvFile;

    /*
     * Calculate and print the MD5 sum for one file.
     */
    pvFile = open_file(pszFilename, fText);
    if (pvFile)
    {
        unsigned char Digest[16];

        if (fProgress && pOutput)
            fprintf(stdout, "%s: ", pszFilename);

        rc = calc_md5sum(pvFile, Digest, fProgress);
        close_file(pvFile);

        if (fProgress && pOutput)
        {
            size_t cch = strlen(pszFilename) + 2;
            while (cch-- > 0)
                fputc('\b', stdout);
        }

        if (!rc)
        {
            char szDigest[36];
            digest_to_string(Digest, szDigest);
            if (pOutput)
            {
                fprintf(pOutput, "%s %s%s\n", szDigest, fText ? "" : "*", pszFilename);
                fflush(pOutput);
            }
            fprintf(stdout, "%s %s%s\n", szDigest, fText ? "" : "*", pszFilename);
            fflush(stdout);
        }
        else
        {
            if (!fQuiet)
                errx(1, "Failed to open '%s': %s", pszFilename, strerror(rc));
            rc = 1;
        }
    }
    else
    {
        if (!fQuiet)
            errx(1, "Failed to open '%s': %s", pszFilename, strerror(errno));
        rc = 1;
    }
    return rc;
}



/**
 * md5sum, calculates and checks the md5sum of files.
 * Somewhat similar to the GNU coreutil md5sum command.
 */
int kmk_builtin_md5sum(int argc, char **argv, char **envp)
{
    int i;
    int rc = 0;
    int fText = 0;
    int fBinaryTextOpt = 0;
    int fQuiet = 0;
    int fChecking = 0;
    int fProgress = 0;
    int fNoMoreOptions = 0;
    const char *pszOutput = NULL;
    FILE *pOutput = NULL;

    g_progname = argv[0];

    /*
     * Print usage if no arguments.
     */
    if (argc <= 1)
        return usage(stderr);

    /*
     * Process the arguments, FIFO style.
     */
    i = 1;
    while (i < argc)
    {
        char *psz = argv[i];
        if (!fNoMoreOptions && psz[0] == '-' && psz[1] == '-' && !psz[2])
            fNoMoreOptions = 1;
        else if (*psz == '-' && !fNoMoreOptions)
        {
            psz++;

            /* convert long options for gnu just for fun */
            if (*psz == '-')
            {
                if (!strcmp(psz, "-binary"))
                    psz = "b";
                else if (!strcmp(psz, "-text"))
                    psz = "t";
                else if (!strcmp(psz, "-check"))
                    psz = "c";
                else if (!strcmp(psz, "-check-file"))
                    psz = "C";
                else if (!strcmp(psz, "-output"))
                    psz = "o";
                else if (!strcmp(psz, "-progress"))
                    psz = "p";
                else if (!strcmp(psz, "-status"))
                    psz = "q";
                else if (!strcmp(psz, "-warn"))
                    psz = "w";
                else if (!strcmp(psz, "-help"))
                    psz = "h";
                else if (!strcmp(psz, "-version"))
                    psz = "v";
            }

            /* short options */
            do
            {
                switch (*psz)
                {
                    case 'c':
                        fChecking = 1;
                        break;

                    case 'b':
                        fText = 0;
                        fBinaryTextOpt = 1;
                        break;

                    case 't':
                        fText = 1;
                        fBinaryTextOpt = 1;
                        break;

                    case 'p':
                        fProgress = 1;
                        break;

                    case 'q':
                        fQuiet = 1;
                        break;

                    case 'w':
                        /* ignored */
                        break;

                    case 'h':
                        usage(stdout);
                        return 0;

                    case 'v':
                        return kbuild_version(argv[0]);

                    /*
                     * -C md5 file
                     */
                    case 'C':
                    {
                        const char *pszFilename;
                        const char *pszDigest;

                        if (psz[1])
                            pszDigest = &psz[1];
                        else if (i + 1 < argc)
                            pszDigest = argv[++i];
                        else
                        {
                            errx(1, "'-C' is missing the MD5 sum!");
                            return 1;
                        }
                        if (i + 1 < argc)
                            pszFilename = argv[++i];
                        else
                        {
                            errx(1, "'-C' is missing the filename!");
                            return 1;
                        }

                        rc |= check_one_file(pszFilename, pszDigest, fText, fQuiet, fProgress && !fQuiet);
                        psz = "\0";
                        break;
                    }

                    /*
                     * Output file.
                     */
                    case 'o':
                    {
                        if (fChecking)
                        {
                            errx(1, "'-o' cannot be used with -c or -C!");
                            return 1;
                        }

                        if (psz[1])
                            pszOutput = &psz[1];
                        else if (i + 1 < argc)
                            pszOutput = argv[++i];
                        else
                        {
                            errx(1, "'-o' is missing the file name!");
                            return 1;
                        }

                        psz = "\0";
                        break;
                    }

                    default:
                        errx(1, "Invalid option '%c'! (%s)", *psz, argv[i]);
                        return usage(stderr);
                }
            } while (*++psz);
        }
        else if (fChecking)
            rc |= check_files(argv[i], fText, fBinaryTextOpt, fQuiet, fProgress && !fQuiet);
        else
        {
            /* lazily open the output if specified. */
            if (pszOutput)
            {
                if (pOutput)
                    fclose(pOutput);
                pOutput = fopen(pszOutput, "w");
                if (!pOutput)
                {
                    rc = err(1, "fopen(\"%s\", \"w\") failed", pszOutput);
                    break;
                }
                pszOutput = NULL;
            }

            rc |= md5sum_file(argv[i], fText, fQuiet, fProgress && !fQuiet, pOutput);
        }
        i++;
    }

    if (pOutput)
        fclose(pOutput);
    return rc;
}

