/*
 * We do not include "config.h"
 *  Reason 1: we can assume that configure already raised an error if the
 *            required functions were not found.
 *  Reason 2: some macros conflict with ones in <mutil/mhash_config.h>
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <mhash.h>
#include "mhd_common.h"
#include "mhd_command_help.h"

/* Workaround for mhash_end_m */
void*
mhd_malloc_word32(mutils_word32 size)
{
    return malloc(size);
}

/* Calculate a digest */
unsigned char*
mhd_digest(size_t id, const char* file)
{
    FILE*          handle = NULL;
    MHASH          hash   = NULL;
    unsigned char* buffer = NULL;
    size_t         size   = 0;

    hash = mhash_init(id);
    if (hash == MHASH_FAILED) {
        fprintf(stderr, "Could not mhash_init\n");
        return NULL;
    }
 
    /* file */
    if (file) {
        if (! (handle = fopen(file, "rb"))) {
            fprintf(stderr, "Could not open [%s]\n", file);
            mhash_end(hash);
            return NULL;
        }
    }
    /* stdin */
    else {
        handle = stdin;
    }

    if (! (buffer = malloc(MHD_FILE_BUFFER_SIZE)))  {
        fprintf(stderr, "Could not malloc\n");
        mhash_end(hash);
        fclose(handle);
        return NULL;
    }

    while ((size = fread(buffer, 1, MHD_FILE_BUFFER_SIZE, handle)) > 0) {
        mhash(hash, buffer, size);
    }

    free(buffer);
    fclose(handle);
    return mhash_end_m(hash, mhd_malloc_word32);
}

/* Print a digest */
int
mhd_print(const char* name, size_t id, const char* file)
{
    unsigned char* d = NULL;
    int            i = 0;

    if (! (d = mhd_digest(id, file))) {
        return 0;
    }

    if (file) {
        printf("%s(%s)= ", name, file);
    }
    for (i = 0; i < mhash_get_block_size(id); ++i) {
        printf("%.2x", d[i]);
    }
    printf("\n");

    free(d);
    return 1;
}

/* Trim (note that this function modify string) */
char*
mhd_trim(char* s)
{
    size_t i = 0;

    for (i = strlen(s) - 1; i >= 0; --i) {
        if (! isspace(s[i])) {
            break;
        } else {
            s[i] = '\0';
        }
    }

    for (i = 0; i < strlen(s); ++i) {
        if (! isspace(s[i])) {
            break;
        }
    }

    return s + i;
}

int
mhd_parse_bsd(char* buffer, char* name, char* file, char* dgst)
{
    size_t size = 0;
    char*  lp   = NULL;
    char*  rp   = NULL;
    char*  eq   = NULL;
    int    ok   = 0;

    size = strlen (buffer);
    lp   = strchr (buffer, '(');
    rp   = strrchr(buffer, ')');
    eq   = strrchr(buffer, '=');

    ok = lp && rp && eq
        && buffer < lp
        && lp + 1 < rp
        && rp     < eq
        && eq + 1 < buffer + size - 1;
    if (! ok) {
        return 0;
    }

    *lp++ = '\0';
    *rp++ = '\0';
    *eq++ = '\0';

    strncpy(name, mhd_trim(buffer), MHD_TEXT_BUFFER_SIZE);
    strncpy(file, mhd_trim(lp),     MHD_TEXT_BUFFER_SIZE);
    strncpy(dgst, mhd_trim(eq),     MHD_TEXT_BUFFER_SIZE);

    return 1;
}

int
mhd_parse_gnu(char* buffer, char* name, char* file, char* dgst)
{
    size_t size = 0;
    char*  sp   = NULL;
    int    ok   = 0;

    size = strlen(buffer);
    sp   = strchr(buffer, ' ');

    ok = sp
        && buffer < sp
        && sp + 1 < buffer + size - 1;

    *sp++ = '\0';
    if (*sp == '*') {
        sp++;
    }

    strncpy(dgst, mhd_trim(buffer), MHD_TEXT_BUFFER_SIZE);
    strncpy(file, mhd_trim(sp),     MHD_TEXT_BUFFER_SIZE);

    size = strlen(dgst);
    switch (size) {
        case 32:
            strncpy(name, "MD5", MHD_TEXT_BUFFER_SIZE);
            break;
        case 40:
            strncpy(name, "SHA1", MHD_TEXT_BUFFER_SIZE);
            break;
        default:
            return 0;
    }

    return 1;
}

int
mhd_parse(char* buffer, char* name, char* file, char* dgst)
{
    if (strchr(buffer, '(')) {
        return mhd_parse_bsd(buffer, name, file, dgst);
    } else {
        return mhd_parse_gnu(buffer, name, file, dgst);
    }
}

int
mhd_command_list()
{
    const char* static_name = NULL;
    size_t      i           = 0;

    for (i = 0; i <= mhash_count(); ++i) {
        if ((static_name = (const char*) mhash_get_hash_name_static(i))) {
            printf("%s\n", static_name);
        }
    }

    return 0; /* always succeed */
}

int
mhd_command_check(const char* check_file)
{
    FILE*          handle    = NULL;
    char*          buffer    = NULL;
    char*          name      = NULL;
    char*          file      = NULL;
    char*          dgst      = NULL;
    size_t         size      = 0;
    size_t         id        = MHD_NOT_FOUND;
    int            pass      = 1;
    int            pass_each = 1;
    unsigned char* d         = NULL;
    int            i         = 0;
    char           hex[3];

    /* file */
    if (check_file) {
        if (! (handle = fopen(check_file, "rb"))) {
            fprintf(stderr, "Could not open [%s]\n", check_file);
            return 1;
        }
    }
    /* stdin */
    else {
        handle = stdin;
    }

    buffer = malloc(MHD_TEXT_BUFFER_SIZE);
    name   = malloc(MHD_TEXT_BUFFER_SIZE);
    file   = malloc(MHD_TEXT_BUFFER_SIZE);
    dgst   = malloc(MHD_TEXT_BUFFER_SIZE);

    if (! (buffer && name && file && dgst)) {
        fprintf(stderr, "Could not malloc\n");
        free(buffer);
        free(name);
        free(file);
        free(dgst);
        fclose(handle);
        return 1;
    }

    while (! feof(handle)) {
        if (! fgets(buffer, MHD_TEXT_BUFFER_SIZE, handle)) {
            break;
        }

        /* chomp */
        size = strlen(buffer);
        if (buffer[size - 1] == '\n') {
            buffer[size - 1] = '\0';
        }

        pass_each = 1;

        memset(name, 0, MHD_TEXT_BUFFER_SIZE);
        memset(file, 0, MHD_TEXT_BUFFER_SIZE);
        memset(dgst, 0, MHD_TEXT_BUFFER_SIZE);

        if (! mhd_parse(buffer, name, file, dgst)) {
            fprintf(stderr, "Parse error [%s]\n", buffer);
            pass = pass_each = 0;
        }
        else if ((id = mhd_find(name)) == MHD_NOT_FOUND) {
            fprintf(stderr, "Could not find [%s]\n", name);
            pass = pass_each = 0;
        }
        else if (strlen(dgst) != mhash_get_block_size(id) * 2) {
            pass = pass_each = 0;
        }
        else if (! (d = mhd_digest(id, file))) {
            pass = pass_each = 0;
        }
        else {
            for (i = 0; i < mhash_get_block_size(id); ++i) {
                snprintf(hex, 3, "%.2x", d[i]);
                if (hex[0] != dgst[i*2] || hex[1] != dgst[i*2+1]) {
                    pass = pass_each = 0;
                    break;
                }
            }
            free(d);
        }

        printf("%s %s\n", (pass_each ? "PASS" : "FAIL"), file);
    }

    free(buffer);
    free(name);
    free(file);
    free(dgst);
    fclose(handle);

    return ! pass;
}

int
mhd_command_main(char* name, char* const* file_first, char* const* file_last)
{
    size_t       id   = MHD_NOT_FOUND;
    char* const* file = NULL;

    if ((id = mhd_find(name)) == MHD_NOT_FOUND) {
        fprintf(stderr, "Could not find [%s]\n", name);
        return 1;
    }

    /* files */
    if (file_first < file_last) {
        for (file = file_first; file != file_last; ++file) {
            if (! mhd_print(name, id, *file)) {
                return 1;
            }
        }
    }
    /* stdin */
    else {
        if (! mhd_print(name, id, NULL)) {
            return 1;
        }
    }

    return 0;
}

int
main(int ac, char** av)
{
    char*        name       = "SHA512";
    int          c          = -1;
    int          check      = 0;
    int          bench      = 0;
    const char*  file       = NULL;
    char* const* file_first = NULL;
    char* const* file_last  = NULL;

    opterr = 1;

    while ((c = getopt(ac, av, "a:bchl")) != -1) {
        switch (c) {
            case 'a':
                name = optarg;
                break;
            case 'b':
                bench = 1;
                break;
            case 'c':
                check = 1;
                break;
            case 'h':
                return mhd_command_help();
            case 'l':
                return mhd_command_list();
            default:
                return mhd_command_help();
        }
    }

    file_first = av + optind;
    file_last  = av + ac;
    if (file_first < file_last) {
        file = *file_first;
    }

    if (check) {
        return mhd_command_check(file);
    } else {
        return mhd_command_main(name, file_first, file_last);
    }
}
