/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id$
 *
 * Copyright (C) 2015 by Cástor Muñoz
 *
 * 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 software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#include "mks5lboot.h"

/* Header for ARM code binaries */
#include "dualboot.h"

/* Win32 compatibility */
#ifndef O_BINARY
#define O_BINARY 0
#endif

/*
 * This code is based on emCORE, adds a couple of improvements thanks to
 * some extra features in Apple's ROM:
 * - Generic Im3Info header valid for all payloads. It is done by moving
 *   the certificate to a fixed position (before data), and using a 'magic'
 *   value (0x300) for signature offset.
 * - Some integer overflows in ROM make it possible to use the free space
 *   available in Im3Hdr, resulting a maximum payload size of:
 *   128 KiB - 0x50 bytes (IM3INFO_SZ) - 700 bytes (CERT_SIZE).
 */

const struct ipod_models ipod_identity[] =
{
    [MODEL_IPOD6G] = {
        "Classic 6G", "ipod6g", "ip6g", 71,
        dualboot_install_ipod6g,   sizeof(dualboot_install_ipod6g),
        dualboot_uninstall_ipod6g, sizeof(dualboot_uninstall_ipod6g) },
};

struct Im3Info s5l8702hdr =
{
    .ident          = IM3_IDENT,
    .version        = IM3_VERSION,
    .enc_type       = 3,
    .u.enc34 = {
        .sign_off   = "\x00\x03\x00\x00",
        .cert_off   = "\x50\xF8\xFF\xFF", /* -0x800 + CERT_OFFSET */
        .cert_sz    = "\xBA\x02\x00\x00", /* CERT_SIZE */
    },
    .info_sign      = "\xC2\x54\x51\x31\xDC\xC0\x84\xA4"
                      "\x7F\xD1\x45\x08\xE8\xFF\xE8\x1D",
};

unsigned char s5l8702pwnage[/*CERT_SIZE*/] =
{
    "\x30\x82\x00\x7A\x30\x66\x02\x00\x30\x0D\x06\x09\x2A\x86\x48\x86"
    "\xF7\x0D\x01\x01\x05\x05\x00\x30\x0B\x31\x09\x30\x07\x06\x03\x55"
    "\x04\x03\x13\x00\x30\x1E\x17\x0D\x00\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x00\x00\x00\x17\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x00\x00\x30\x0B\x31\x09\x30\x07\x06\x03\x55\x04\x03\x13"
    "\x00\x30\x19\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01"
    "\x05\x00\x03\x08\x00\x30\x05\x02\x01\x00\x02\x00\x30\x0D\x06\x09"
    "\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05\x05\x00\x03\x01\x00\x30\x82"
    "\x00\x7A\x30\x66\x02\x00\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D"
    "\x01\x01\x05\x05\x00\x30\x0B\x31\x09\x30\x07\x06\x03\x55\x04\x03"
    "\x13\x00\x30\x1E\x17\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x00\x17\x0D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x30\x0B\x31\x09\x30\x07\x06\x03\x55\x04\x03\x13\x00\x30"
    "\x19\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00"
    "\x03\x08\x00\x30\x05\x02\x01\x00\x02\x00\x30\x0D\x06\x09\x2A\x86"
    "\x48\x86\xF7\x0D\x01\x01\x05\x05\x00\x03\x01\x00\x30\x82\x01\xBA"
    "\x30\x50\x02\x00\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01"
    "\x05\x05\x00\x30\x00\x30\x1E\x17\x0D\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x00\x00\x00\x00\x17\x0D\x00\x00\x00\x00\x00\x00\x00\x00"
    "\x00\x00\x00\x00\x00\x30\x00\x30\x19\x30\x0D\x06\x09\x2A\x86\x48"
    "\x86\xF7\x0D\x01\x01\x01\x05\x00\x03\x08\x00\x30\x05\x02\x01\x00"
    "\x02\x00\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05\x05"
    "\x00\x03\x82\x01\x55"
};

static uint32_t get_uint32le(unsigned char* p)
{
    return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24);
}

static uint32_t get_uint32be(unsigned char* p)
{
    return (p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3];
}

static void put_uint32le(unsigned char* p, uint32_t x)
{
    p[0] = x & 0xff;
    p[1] = (x >> 8) & 0xff;
    p[2] = (x >> 16) & 0xff;
    p[3] = (x >> 24) & 0xff;
}

#define ERROR(format, ...) \
    do { \
        snprintf(errstr, errstrsize, "[ERR] "format, __VA_ARGS__); \
        goto error; \
    } while(0)

static unsigned char *load_file(char *filename, int *bufsize,
                                const struct ipod_models** model,
                                char* errstr, int errstrsize)
{
    int fd, i;
    struct stat s;
    unsigned char header[8];
    unsigned char *buf = NULL;
    bool is_rbbl = (model);

    fd = open(filename, O_RDONLY|O_BINARY);
    if (fd < 0)
        ERROR("Could not open %s for reading", filename);

    if (fstat(fd, &s) < 0)
        ERROR("Checking filesize of input file %s", filename);
    *bufsize = s.st_size;

    if (is_rbbl) {
        /* Read Rockbox header */
        if (read(fd, header, sizeof(header)) != sizeof(header))
            ERROR("Could not read file %s", filename);
        *bufsize -= sizeof(header);

        for (i = 0; i < NUM_MODELS; i++)
            if (memcmp(ipod_identity[i].rb_model_name, header + 4, 4) == 0)
                break;

        if (i == NUM_MODELS)
            ERROR("Model name \"%4.4s\" unknown. "
                    "Is this really a rockbox bootloader?", header + 4);

        *model = &ipod_identity[i];
    }

    buf = malloc(*bufsize);
    if (buf == NULL)
        ERROR("Could not allocate memory for %s", filename);

    if (read(fd, buf, *bufsize) != *bufsize)
        ERROR("Could not read file %s", filename);

    if (is_rbbl) {
        /* Check checksum */
        uint32_t sum = (*model)->rb_model_num;
        for (i = 0; i < *bufsize; i++) {
             /* add 8 unsigned bits but keep a 32 bit sum */
             sum += buf[i];
        }
        if (sum != get_uint32be(header))
            ERROR("Checksum mismatch in %s", filename);
    }

    close(fd);
    return buf;

error:
    if (fd >= 0)
        close(fd);
    if (buf)
        free(buf);
    return NULL;
}

unsigned char *mkdfu(int dfu_type, char *dfu_arg, int* dfu_size,
                                        char* errstr, int errstrsize)
{
    const struct ipod_models *model = NULL;
    unsigned char *dfu_buf = NULL;
    unsigned char *f_buf;
    int f_size;
    uint32_t padded_bl_size;
    uint32_t cert_off, cert_sz;
    off_t cur_off;
    char *dfu_desc;
    int i;

    switch (dfu_type)
    {
        case DFU_INST:
        case DFU_INST_SINGLE:
            f_buf = load_file(dfu_arg, &f_size, &model, errstr, errstrsize);
            if (f_buf == NULL)
                goto error;
            /* IM3 data size should be padded to 16 */
            padded_bl_size = ((f_size + 0xf) & ~0xf);
            *dfu_size = BIN_OFFSET + (model->dualboot_install_size +
                         (IM3HDR_SZ - (int)IM3INFO_SZ) + (int)padded_bl_size);
            dfu_desc = (dfu_type == DFU_INST_SINGLE) ? "BL installer (single)"
                                                     : "BL installer";
            break;

        case DFU_UNINST:
            for (i = 0; i < NUM_MODELS; i++) {
                if (!strcmp(ipod_identity[i].platform_name, dfu_arg)) {
                    model = &ipod_identity[i];
                    break;
                }
            }
            if (!model)
                ERROR("Platform name \"%s\" unknown", dfu_arg);

            *dfu_size = BIN_OFFSET + model->dualboot_uninstall_size;
            dfu_desc = "BL uninstaller";
            break;

        case DFU_RAW:
        default:
            f_buf = load_file(dfu_arg, &f_size, NULL, errstr, errstrsize);
            if (f_buf == NULL)
                goto error;
            /* IM3 data size should be padded to 16 */
            *dfu_size = BIN_OFFSET + ((f_size + 0xf) & ~0xf);
            dfu_desc = "raw binary";
            break;
    }

    printf("[INFO] Building DFU:\n");
    printf("[INFO]  type:         %s\n", dfu_desc);
    if ((dfu_type == DFU_INST) || (dfu_type == DFU_INST_SINGLE))
        printf("[INFO]  BL size:      %d\n", f_size);
    if (dfu_type == DFU_RAW)
        printf("[INFO]  BIN size:     %d\n", f_size);
    printf("[INFO]  DFU size:     %d\n", *dfu_size);
    if (model) {
        printf("[INFO]  model name:   %s\n", model->model_name);
        printf("[INFO]  platform:     %s\n", model->platform_name);
        printf("[INFO]  RB name:      %s\n", model->rb_model_name);
        printf("[INFO]  RB num:       %d\n", model->rb_model_num);
    }

    if (*dfu_size > DFU_MAXSIZE)
        ERROR("DFU image (%d bytes) too big", *dfu_size);

    dfu_buf = calloc(*dfu_size, 1);
    if (!dfu_buf)
        ERROR("Could not allocate %d bytes for DFU image", *dfu_size);

    cert_off = get_uint32le(s5l8702hdr.u.enc34.cert_off);
    cert_sz  = get_uint32le(s5l8702hdr.u.enc34.cert_sz);

    memcpy(dfu_buf, &s5l8702hdr, sizeof(s5l8702hdr));

    cur_off = IM3HDR_SZ + cert_off;

    memcpy(dfu_buf + cur_off, s5l8702pwnage, sizeof(s5l8702pwnage));

    /* set entry point */
    cur_off += cert_sz - 4;
    put_uint32le(dfu_buf + cur_off, DFU_LOADADDR + BIN_OFFSET);

    cur_off = BIN_OFFSET;

    switch (dfu_type)
    {
        case DFU_INST:
        case DFU_INST_SINGLE:
            /* copy the dualboot installer binary */
            memcpy(dfu_buf + cur_off, model->dualboot_install,
                                        model->dualboot_install_size);
            /* point to the start of the included IM3 header info */
            cur_off += model->dualboot_install_size - IM3INFO_SZ;
            /* set bootloader data size */
            struct Im3Info *bl_info = (struct Im3Info*)(dfu_buf + cur_off);
            put_uint32le(bl_info->data_sz, padded_bl_size);
            /* use info_sign to pass params to the dualboot installer */
            if (dfu_type == DFU_INST_SINGLE)
                bl_info->info_sign[0] = 0x1;
            /* add bootloader binary */
            cur_off += IM3HDR_SZ;
            memcpy(dfu_buf + cur_off, f_buf, f_size);
            break;

        case DFU_UNINST:
            /* copy the dualboot uninstaller binary */
            memcpy(dfu_buf + cur_off, model->dualboot_uninstall,
                                        model->dualboot_uninstall_size);
            break;

        case DFU_RAW:
        default:
            /* add raw binary */
            memcpy(dfu_buf + cur_off, f_buf, f_size);
            break;
    }

    return dfu_buf;

error:
    if (dfu_buf)
        free(dfu_buf);
    return NULL;
}
