/* ----------------------------------------------------------------------- *
 *
 *   Copyright 2003-2009 H. Peter Anvin - All Rights Reserved
 *   Copyright 2009-2010 Intel Corporation; author: H. Peter Anvin
 *   Copyright (C) 2010 Shao Miller
 *
 *   Permission is hereby granted, free of charge, to any person
 *   obtaining a copy of this software and associated documentation
 *   files (the "Software"), to deal in the Software without
 *   restriction, including without limitation the rights to use,
 *   copy, modify, merge, publish, distribute, sublicense, and/or
 *   sell copies of the Software, and to permit persons to whom
 *   the Software is furnished to do so, subject to the following
 *   conditions:
 *
 *   The above copyright notice and this permission notice shall
 *   be included in all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 *   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 *   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 *   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 *   OTHER DEALINGS IN THE SOFTWARE.
 *
 * ----------------------------------------------------------------------- */

/**
 * @file disk.c
 *
 * Deal with disks and partitions
 */

#include <core.h>
#include <dprintf.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslinux/disk.h>

/**
 * Call int 13h, but with retry on failure.  Especially floppies need this.
 *
 * @v inreg			CPU register settings upon INT call
 * @v outreg			CPU register settings returned by INT call
 * @ret (int)			0 upon success, -1 upon failure
 */
int disk_int13_retry(const com32sys_t * inreg, com32sys_t * outreg)
{
    int retry = 6;		/* Number of retries */
    com32sys_t tmpregs;

    if (!outreg)
	outreg = &tmpregs;

    while (retry--) {
	__intcall(0x13, inreg, outreg);
	if (!(outreg->eflags.l & EFLAGS_CF))
	    return 0;		/* CF=0, OK */
    }

    return -1;			/* Error */
}

/**
 * Query disk parameters and EBIOS availability for a particular disk.
 *
 * @v disk			The INT 0x13 disk drive number to process
 * @v diskinfo			The structure to save the queried params to
 * @ret (int)			0 upon success, -1 upon failure
 */
int disk_get_params(int disk, struct disk_info *const diskinfo)
{
    static com32sys_t inreg, outreg;
    struct disk_ebios_eparam *eparam;
    int rv = 0;

    memset(diskinfo, 0, sizeof *diskinfo);
    diskinfo->disk = disk;
    diskinfo->bps = SECTOR;

    /* Get EBIOS support */
    memset(&inreg, 0, sizeof inreg);
    inreg.eax.b[1] = 0x41;
    inreg.ebx.w[0] = 0x55aa;
    inreg.edx.b[0] = disk;
    inreg.eflags.b[0] = 0x3;	/* CF set */

    __intcall(0x13, &inreg, &outreg);

    if (!(outreg.eflags.l & EFLAGS_CF) &&
	outreg.ebx.w[0] == 0xaa55 && (outreg.ecx.b[0] & 1)) {
	diskinfo->ebios = 1;
    }

    eparam = lmalloc(sizeof *eparam);
    if (!eparam)
	return -1;

    /* Get extended disk parameters if ebios == 1 */
    if (diskinfo->ebios) {
	memset(&inreg, 0, sizeof inreg);
	inreg.eax.b[1] = 0x48;
	inreg.edx.b[0] = disk;
	inreg.esi.w[0] = OFFS(eparam);
	inreg.ds = SEG(eparam);

	memset(eparam, 0, sizeof *eparam);
	eparam->len = sizeof *eparam;

	__intcall(0x13, &inreg, &outreg);

	if (!(outreg.eflags.l & EFLAGS_CF)) {
	    diskinfo->lbacnt = eparam->lbacnt;
	    if (eparam->bps)
		diskinfo->bps = eparam->bps;
	    /*
	     * don't think about using geometry data returned by
	     * 48h, as it can differ from 08h a lot ...
	     */
	}
    }
    /*
     * Get disk parameters the old way - really only useful for hard
     * disks, but if we have a partitioned floppy it's actually our best
     * chance...
     */
    memset(&inreg, 0, sizeof inreg);
    inreg.eax.b[1] = 0x08;
    inreg.edx.b[0] = disk;

    __intcall(0x13, &inreg, &outreg);

    if (outreg.eflags.l & EFLAGS_CF) {
	rv = diskinfo->ebios ? 0 : -1;
	goto out;
    }

    diskinfo->spt = 0x3f & outreg.ecx.b[0];
    diskinfo->head = 1 + outreg.edx.b[1];
    diskinfo->cyl = 1 + (outreg.ecx.b[1] | ((outreg.ecx.b[0] & 0xc0u) << 2));

    if (diskinfo->spt)
	diskinfo->cbios = 1;	/* Valid geometry */
    else {
	diskinfo->head = 1;
	diskinfo->spt = 1;
	diskinfo->cyl = 1;
    }

    if (!diskinfo->lbacnt)
	diskinfo->lbacnt = diskinfo->cyl * diskinfo->head * diskinfo->spt;

out:
    lfree(eparam);
    return rv;
}

/**
 * Fill inreg based on EBIOS addressing properties.
 *
 * @v diskinfo			The disk drive to read from
 * @v inreg			Register data structure to be filled.
 * @v lba			The logical block address to begin reading at
 * @v count			The number of sectors to read
 * @v op_code			Code to write/read operation
 * @ret 			lmalloc'd buf upon success, NULL upon failure
 */
static void *ebios_setup(const struct disk_info *const diskinfo, com32sys_t *inreg,
			 uint64_t lba, uint8_t count, uint8_t op_code)
{
    static struct disk_ebios_dapa *dapa = NULL;
    void *buf;

    if (!dapa) {
	dapa = lmalloc(sizeof *dapa);
	if (!dapa)
	    return NULL;
    }

    buf = lmalloc(count * diskinfo->bps);
    if (!buf)
	return NULL;

    dapa->len = sizeof(*dapa);
    dapa->count = count;
    dapa->off = OFFS(buf);
    dapa->seg = SEG(buf);
    dapa->lba = lba;

    inreg->eax.b[1] = op_code;
    inreg->esi.w[0] = OFFS(dapa);
    inreg->ds = SEG(dapa);
    inreg->edx.b[0] = diskinfo->disk;

    return buf;
}

/**
 * Fill inreg based on CHS addressing properties.
 *
 * @v diskinfo			The disk drive to read from
 * @v inreg			Register data structure to be filled.
 * @v lba			The logical block address to begin reading at
 * @v count			The number of sectors to read
 * @v op_code			Code to write/read operation
 * @ret 			lmalloc'd buf upon success, NULL upon failure
 */
static void *chs_setup(const struct disk_info *const diskinfo, com32sys_t *inreg,
		       uint64_t lba, uint8_t count, uint8_t op_code)
{
    unsigned int c, h, s, t;
    void *buf;

    buf = lmalloc(count * diskinfo->bps);
    if (!buf)
	return NULL;

    /*
     * if we passed lba + count check and we get here, that means that
     * lbacnt was calculated from chs geometry (or faked from 1/1/1), thus
     * 32bits are perfectly enough and lbacnt corresponds to cylinder
     * boundary
     */
    s = lba % diskinfo->spt;
    t = lba / diskinfo->spt;
    h = t % diskinfo->head;
    c = t / diskinfo->head;

    inreg->eax.b[0] = count;
    inreg->eax.b[1] = op_code;
    inreg->ecx.b[1] = c;
    inreg->ecx.b[0] = ((c & 0x300) >> 2) | (s+1);
    inreg->edx.b[1] = h;
    inreg->edx.b[0] = diskinfo->disk;
    inreg->ebx.w[0] = OFFS(buf);
    inreg->es = SEG(buf);

    return buf;
}

/**
 * Get disk block(s) and return a malloc'd buffer.
 *
 * @v diskinfo			The disk drive to read from
 * @v lba			The logical block address to begin reading at
 * @v count			The number of sectors to read
 * @ret data			An allocated buffer with the read data
 *
 * Uses the disk number and information from diskinfo.  Read count sectors
 * from drive, starting at lba.  Return a new buffer, or NULL upon failure.
 */
void *disk_read_sectors(const struct disk_info *const diskinfo, uint64_t lba,
			uint8_t count)
{
    com32sys_t inreg;
    void *buf;
    void *data = NULL;
    uint32_t maxcnt;
    uint32_t size = 65536;

    maxcnt = (size - diskinfo->bps) / diskinfo->bps;
    if (!count || count > maxcnt || lba + count > diskinfo->lbacnt)
	return NULL;

    memset(&inreg, 0, sizeof inreg);

    if (diskinfo->ebios)
	buf = ebios_setup(diskinfo, &inreg, lba, count, EBIOS_READ_CODE);
    else
	buf = chs_setup(diskinfo, &inreg, lba, count, CHS_READ_CODE);

    if (!buf)
	return NULL;

    if (disk_int13_retry(&inreg, NULL))
	goto out;

    data = malloc(count * diskinfo->bps);
    if (data)
	memcpy(data, buf, count * diskinfo->bps);
out:
    lfree(buf);
    return data;
}

/**
 * Write disk block(s).
 *
 * @v diskinfo			The disk drive to write to
 * @v lba			The logical block address to begin writing at
 * @v data			The data to write
 * @v count			The number of sectors to write
 * @ret (int)			0 upon success, -1 upon failure
 *
 * Uses the disk number and information from diskinfo.
 * Write sector(s) to a disk drive, starting at lba.
 */
int disk_write_sectors(const struct disk_info *const diskinfo, uint64_t lba,
		       const void *data, uint8_t count)
{
    com32sys_t inreg;
    void *buf;
    uint32_t maxcnt;
    uint32_t size = 65536;
    int rv = -1;

    maxcnt = (size - diskinfo->bps) / diskinfo->bps;
    if (!count || count > maxcnt || lba + count > diskinfo->lbacnt)
	return -1;

    memset(&inreg, 0, sizeof inreg);

    if (diskinfo->ebios)
	buf = ebios_setup(diskinfo, &inreg, lba, count, EBIOS_WRITE_CODE);
    else
	buf = chs_setup(diskinfo, &inreg, lba, count, CHS_WRITE_CODE);

    if (!buf)
	return -1;

    memcpy(buf, data, count * diskinfo->bps);

    if (disk_int13_retry(&inreg, NULL))
	goto out;

    rv = 0;			/* ok */
out:
    lfree(buf);
    return rv;
}

/**
 * Write disk blocks and verify they were written.
 *
 * @v diskinfo			The disk drive to write to
 * @v lba			The logical block address to begin writing at
 * @v buf			The data to write
 * @v count			The number of sectors to write
 * @ret rv			0 upon success, -1 upon failure
 *
 * Uses the disk number and information from diskinfo.
 * Writes sectors to a disk drive starting at lba, then reads them back
 * to verify they were written correctly.
 */
int disk_write_verify_sectors(const struct disk_info *const diskinfo,
			      uint64_t lba, const void *buf, uint8_t count)
{
    char *rb;
    int rv;

    rv = disk_write_sectors(diskinfo, lba, buf, count);
    if (rv)
	return rv;		/* Write failure */
    rb = disk_read_sectors(diskinfo, lba, count);
    if (!rb)
	return -1;		/* Readback failure */
    rv = memcmp(buf, rb, count * diskinfo->bps);
    free(rb);
    return rv ? -1 : 0;
}

/**
 * Dump info about a DOS partition entry
 *
 * @v part			The 16-byte partition entry to examine
 */
void disk_dos_part_dump(const struct disk_dos_part_entry *const part)
{
    (void)part;
    dprintf("Partition status _____ : 0x%.2x\n"
	    "Partition CHS start\n"
	    "  Cylinder ___________ : 0x%.4x (%u)\n"
	    "  Head _______________ : 0x%.2x (%u)\n"
	    "  Sector _____________ : 0x%.2x (%u)\n"
	    "Partition type _______ : 0x%.2x\n"
	    "Partition CHS end\n"
	    "  Cylinder ___________ : 0x%.4x (%u)\n"
	    "  Head _______________ : 0x%.2x (%u)\n"
	    "  Sector _____________ : 0x%.2x (%u)\n"
	    "Partition LBA start __ : 0x%.8x (%u)\n"
	    "Partition LBA count __ : 0x%.8x (%u)\n"
	    "-------------------------------\n",
	    part->active_flag,
	    chs_cylinder(part->start),
	    chs_cylinder(part->start),
	    chs_head(part->start),
	    chs_head(part->start),
	    chs_sector(part->start),
	    chs_sector(part->start),
	    part->ostype,
	    chs_cylinder(part->end),
	    chs_cylinder(part->end),
	    chs_head(part->end),
	    chs_head(part->end),
	    chs_sector(part->end),
	    chs_sector(part->end),
	    part->start_lba, part->start_lba, part->length, part->length);
}

/* Trivial error message output */
static inline void error(const char *msg)
{
    fputs(msg, stderr);
}

/**
 * This walk-map effectively reverses the little-endian
 * portions of a GPT disk/partition GUID for a string representation.
 * There might be a better header for this...
 */
static const char guid_le_walk_map[] = {
    3, -1, -1, -1, 0,
    5, -1, 0,
    3, -1, 0,
    2, 1, 0,
    1, 1, 1, 1, 1, 1
};

/**
 * Fill a buffer with a textual GUID representation.
 *
 * @v buf			Points to a minimum array of 37 chars
 * @v id			The GUID to represent as text
 *
 * The buffer must be >= char[37] and will be populated
 * with an ASCII NUL C string terminator.
 * Example: 11111111-2222-3333-4444-444444444444
 * Endian:  LLLLLLLL-LLLL-LLLL-BBBB-BBBBBBBBBBBB
 */
void guid_to_str(char *buf, const struct guid *const id)
{
    unsigned int i = 0;
    const char *walker = (const char *)id;

    while (i < sizeof(guid_le_walk_map)) {
	walker += guid_le_walk_map[i];
	if (!guid_le_walk_map[i])
	    *buf = '-';
	else {
	    *buf = ((*walker & 0xF0) >> 4) + '0';
	    if (*buf > '9')
		*buf += 'A' - '9' - 1;
	    buf++;
	    *buf = (*walker & 0x0F) + '0';
	    if (*buf > '9')
		*buf += 'A' - '9' - 1;
	}
	buf++;
	i++;
    }
    *buf = 0;
}

/**
 * Create a GUID structure from a textual GUID representation.
 *
 * @v buf			Points to a GUID string to parse
 * @v id			Points to a GUID to be populated
 * @ret (int)			Returns 0 upon success, -1 upon failure
 *
 * The input buffer must be >= 32 hexadecimal chars and be
 * terminated with an ASCII NUL.  Returns non-zero on failure.
 * Example: 11111111-2222-3333-4444-444444444444
 * Endian:  LLLLLLLL-LLLL-LLLL-BBBB-BBBBBBBBBBBB
 */
int str_to_guid(const char *buf, struct guid *const id)
{
    char guid_seq[sizeof(struct guid) * 2];
    unsigned int i = 0;
    char *walker = (char *)id;

    while (*buf && i < sizeof(guid_seq)) {
	switch (*buf) {
	    /* Skip these three characters */
	case '{':
	case '}':
	case '-':
	    break;
	default:
	    /* Copy something useful to the temp. sequence */
	    if ((*buf >= '0') && (*buf <= '9'))
		guid_seq[i] = *buf - '0';
	    else if ((*buf >= 'A') && (*buf <= 'F'))
		guid_seq[i] = *buf - 'A' + 10;
	    else if ((*buf >= 'a') && (*buf <= 'f'))
		guid_seq[i] = *buf - 'a' + 10;
	    else {
		/* Or not */
		error("Illegal character in GUID!\n");
		return -1;
	    }
	    i++;
	}
	buf++;
    }
    /* Check for insufficient valid characters */
    if (i < sizeof(guid_seq)) {
	error("Too few GUID characters!\n");
	return -1;
    }
    buf = guid_seq;
    i = 0;
    while (i < sizeof(guid_le_walk_map)) {
	if (!guid_le_walk_map[i])
	    i++;
	walker += guid_le_walk_map[i];
	*walker = *buf << 4;
	buf++;
	*walker |= *buf;
	buf++;
	i++;
    }
    return 0;
}

/**
 * Display GPT partition details.
 *
 * @v gpt_part			The GPT partition entry to display
 */
void disk_gpt_part_dump(const struct disk_gpt_part_entry *const gpt_part)
{
    unsigned int i;
    char guid_text[37];

    dprintf("----------------------------------\n"
	    "GPT part. LBA first __ : 0x%.16llx\n"
	    "GPT part. LBA last ___ : 0x%.16llx\n"
	    "GPT part. attribs ____ : 0x%.16llx\n"
	    "GPT part. name _______ : '",
	    gpt_part->lba_first, gpt_part->lba_last, gpt_part->attribs);
    for (i = 0; i < sizeof(gpt_part->name); i++) {
	if (gpt_part->name[i])
	    dprintf("%c", gpt_part->name[i]);
    }
    dprintf("'");
    guid_to_str(guid_text, &gpt_part->type);
    dprintf("GPT part. type GUID __ : {%s}\n", guid_text);
    guid_to_str(guid_text, &gpt_part->uid);
    dprintf("GPT part. unique ID __ : {%s}\n", guid_text);
}

/**
 * Display GPT header details.
 *
 * @v gpt			The GPT header to display
 */
void disk_gpt_header_dump(const struct disk_gpt_header *const gpt)
{
    char guid_text[37];

    printf("GPT sig ______________ : '%8.8s'\n"
	   "GPT major revision ___ : 0x%.4x\n"
	   "GPT minor revision ___ : 0x%.4x\n"
	   "GPT header size ______ : 0x%.8x\n"
	   "GPT header checksum __ : 0x%.8x\n"
	   "GPT reserved _________ : '%4.4s'\n"
	   "GPT LBA current ______ : 0x%.16llx\n"
	   "GPT LBA alternative __ : 0x%.16llx\n"
	   "GPT LBA first usable _ : 0x%.16llx\n"
	   "GPT LBA last usable __ : 0x%.16llx\n"
	   "GPT LBA part. table __ : 0x%.16llx\n"
	   "GPT partition count __ : 0x%.8x\n"
	   "GPT partition size ___ : 0x%.8x\n"
	   "GPT part. table chksum : 0x%.8x\n",
	   gpt->sig,
	   gpt->rev.fields.major,
	   gpt->rev.fields.minor,
	   gpt->hdr_size,
	   gpt->chksum,
	   gpt->reserved1,
	   gpt->lba_cur,
	   gpt->lba_alt,
	   gpt->lba_first_usable,
	   gpt->lba_last_usable,
	   gpt->lba_table, gpt->part_count, gpt->part_size, gpt->table_chksum);
    guid_to_str(guid_text, &gpt->disk_guid);
    printf("GPT disk GUID ________ : {%s}\n", guid_text);
}
