/*
 * Copyright (C) 2001-2017 Red Hat, Inc.
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include "md5.h"

#include "utilities.h"

static unsigned char *read_primary_volume_descriptor(const int fd, off_t *const offset) {
    /*
     * According to ECMA-119 8.1.1.
     */
    enum { BOOT_RECORD = 0,
           PRIMARY = 1,
           ADDITIONAL = 2,
           PARTITION = 3,
           SET_TERMINATOR = 255 };
    off_t nbyte = SYSTEM_AREA_SIZE;
    /* Skip unused system area. */
    if (lseek(fd, nbyte, SEEK_SET) == -1) {
        return NULL;
    }
    unsigned char *sector_buffer;
    sector_buffer = aligned_alloc((size_t) getpagesize(), SECTOR_SIZE * sizeof(*sector_buffer));
    /* Read n volume descriptors. */
    for (;;) {
        if (read(fd, sector_buffer, SECTOR_SIZE) == -1) {
            free(sector_buffer);
            return NULL;
        }
        if (sector_buffer[0] == PRIMARY) {
            break;
        } else if (sector_buffer[0] == SET_TERMINATOR) {
            return NULL;
        }
        nbyte *= SECTOR_SIZE;
    }
    *offset = nbyte;
    return sector_buffer;
}

static off_t isosize(const unsigned char *const buffer) {
    /*
     * Doing multiplications so that it can be guaranteed that the big endian
     * number is converted to the systems endianness without knowing the
     * endianness of the system.
     */
    off_t result = buffer[SIZE_OFFSET] * 0x1000000 +
                   buffer[SIZE_OFFSET + 1] * 0x10000 +
                   buffer[SIZE_OFFSET + 2] * 0x100 + buffer[SIZE_OFFSET + 3];
    result *= SECTOR_SIZE;
    return result;
}

static size_t starts_with(const char *const buffer, const char *const string) {
    const size_t len = strlen(string);
    return strncmp(buffer, string, len) ? 0 : len;
}

/**
 * Read and store number from buffer if the buffer starts with string.
 */
static size_t matches_number(char *const buffer, size_t index,
                             const char *const string, long int *const number) {
    size_t len = starts_with(buffer + index, string);
    index += len;
    if (len > 0UL && index < APPDATA_SIZE) {
        char tmp[APPDATA_SIZE];
        /* The number should be every until the semicolon. */
        char *ptr = tmp;
        for (; index < APPDATA_SIZE && buffer[index] != ';';
             ptr++, index++)
            *ptr = buffer[index];
        *ptr = '\0';
        char *endptr;
        *number = strtol(tmp, &endptr, 10);
        return endptr != NULL && *endptr != '\0' ? 0UL : index;
    }
    return 0UL;
}

off_t primary_volume_size(const int isofd, off_t *const offset) {
    unsigned char *buffer = read_primary_volume_descriptor(isofd, offset);
    if (buffer == NULL)
        return 0;
    off_t tmp = isosize(buffer);
    free(buffer);
    return tmp;
}

/* Find the primary volume descriptor and return parsed information from it. */
struct volume_info *const parsepvd(const int isofd) {
    char buffer[APPDATA_SIZE];
    off_t offset;

    enum task_status {
        TASK_SUPPORTED = 1,
        TASK_FRAGCOUNT = 1 << 1,
        TASK_FRAGSUM = 1 << 2,
        TASK_MD5 = 1 << 3,
        TASK_SKIP = 1 << 4,
        TASK_DONE = (1 << 5) - 1
    };
    enum task_status task = 0;

    unsigned char *const aligned_buffer = read_primary_volume_descriptor(isofd, &offset);
    if (aligned_buffer == NULL)
        return NULL;
    /* Application data */
    memcpy(buffer, aligned_buffer + APPDATA_OFFSET, APPDATA_SIZE);
    buffer[APPDATA_SIZE - 1] = '\0';

    struct volume_info *result = malloc(sizeof(struct volume_info));
    result->skipsectors = SKIPSECTORS;
    result->supported = 0;
    result->fragmentcount = FRAGMENT_COUNT;
    result->offset = offset;
    result->isosize = isosize(aligned_buffer);

    free(aligned_buffer);

    for (size_t index = 0; index < APPDATA_SIZE;) {
        size_t len;
        if ((len = starts_with(buffer + index, "ISO MD5SUM = "))) {
            index += len;
            if (index + HASH_SIZE >= APPDATA_SIZE)
                goto fail;

            memcpy(result->hashsum, buffer + index, HASH_SIZE);
            result->hashsum[HASH_SIZE] = '\0';
            index += HASH_SIZE;
            task |= TASK_MD5;
            /* Skip to next semicolon. */
            for (char *p = buffer + index; index < APPDATA_SIZE && *p != ';';
                 p++, index++) {
            }
        } else if ((len = matches_number(buffer, index,
                                         "SKIPSECTORS = ", (long int *) &result->skipsectors))) {
            index = len;
            if (index >= APPDATA_SIZE)
                goto fail;
            task |= TASK_SKIP;
        } else if ((len = matches_number(buffer, index,
                                         "RHLISOSTATUS=", (long int *) &result->supported))) {
            index = len;
            task |= TASK_SUPPORTED;
        } else if ((len = starts_with(buffer + index, "FRAGMENT SUMS = "))) {
            index += len;
            if (index + FRAGMENT_SUM_SIZE >= APPDATA_SIZE)
                goto fail;
            memcpy(result->fragmentsums, buffer + index, FRAGMENT_SUM_SIZE);
            result->fragmentsums[FRAGMENT_SUM_SIZE] = '\0';
            task |= TASK_FRAGSUM;
            index += FRAGMENT_SUM_SIZE;
            for (char *p = buffer + index; index < APPDATA_SIZE && *p != ';';
                 p++, index++) {
            }
        } else if ((len = matches_number(buffer, index, "FRAGMENT COUNT = ", (long int *) &result->fragmentcount))) {
            index = len;
            task |= TASK_FRAGCOUNT;
        }
        /* Either something is wrong or it skips a semicolon. */
        index++;
        if (task == TASK_DONE)
            break;
    }

    if (task < TASK_SKIP + TASK_MD5) {
    fail:
        free(result);
        return NULL;
    }
    return result;
}

/**
 * Finalize the given hashctx to determine the fragment sum which is:
 * 1. Take the first base 16 character that is not zero from the hashsum byte
 * or zero if the byte holds the value zero.
 * 2. Concatenate as three (in this case) characters as described in 1.
 * Append the fragment sum (in this case the three characters) to hashsums if
 * it's provided.
 * Return if the fragment sum is valid. The excepted fragment sum is provided
 * in fragmentsums which is supposed to be all fragmentsums concatenated.
 * The fragment sum of fragmentsums to compare with is determined by fragment.
 */
bool validate_fragment(const MD5_CTX *const hashctx, const size_t fragment,
                       const size_t fragmentsize, const char *const fragmentsums, char *hashsums) {
    unsigned char digest[HASH_SIZE / 2];
    MD5_CTX ctx;
    memcpy(&ctx, hashctx, sizeof(ctx));
    MD5_Final(digest, &ctx);
    size_t j = (fragment - 1) * fragmentsize;
    for (size_t i = 0; i < MIN(fragmentsize, HASH_SIZE / 2); i++) {
        char tmp[3];
        /*
         * Using "3" to avoid compiler warning about truncation.
         */
        snprintf(tmp, 3, "%01x", digest[i]);
        if (hashsums != NULL)
            strncat(hashsums, tmp, 1);
        if (fragmentsums != NULL && tmp[0] != fragmentsums[j++])
            return false;
    }
    return true;
}

/**
 * Finalize hashctx and store it in hashsum in base 16.
 */
void md5sum(char *const hashsum, MD5_CTX *const hashctx) {
    unsigned char digest[HASH_SIZE / 2];
    MD5_Final(digest, hashctx);
    *hashsum = '\0';
    for (size_t i = 0; i < HASH_SIZE / 2; i++) {
        char tmp[3];
        snprintf(tmp, 3, "%02x", digest[i]);
        strncat(hashsum, tmp, 2);
    }
}
