/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010  Free Software Foundation, Inc.
 *
 *  GRUB 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.
 *
 *  GRUB 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 GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/machine/biosdisk.h>
#include <grub/machine/kernel.h>
#include <grub/machine/memory.h>
#include <grub/machine/int.h>
#include <grub/disk.h>
#include <grub/dl.h>
#include <grub/mm.h>
#include <grub/types.h>
#include <grub/misc.h>
#include <grub/err.h>
#include <grub/term.h>
#include <grub/i18n.h>

GRUB_MOD_LICENSE ("GPLv3+");

static int cd_drive = 0;
static int grub_biosdisk_rw_int13_extensions (int ah, int drive, void *dap);

static int grub_biosdisk_get_num_floppies (void)
{
  struct grub_bios_int_registers regs;
  int drive;

  /* reset the disk system first */
  regs.eax = 0;
  regs.edx = 0;
  regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;

  grub_bios_interrupt (0x13, &regs);

  for (drive = 0; drive < 2; drive++)
    {
      regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT | GRUB_CPU_INT_FLAGS_CARRY;
      regs.edx = drive;

      /* call GET DISK TYPE */
      regs.eax = 0x1500;
      grub_bios_interrupt (0x13, &regs);
      if (regs.flags & GRUB_CPU_INT_FLAGS_CARRY)
	break;

      /* check if this drive exists */
      if (!(regs.eax & 0x300))
	break;
    }

  return drive;
}

/*
 *   Call IBM/MS INT13 Extensions (int 13 %ah=AH) for DRIVE. DAP
 *   is passed for disk address packet. If an error occurs, return
 *   non-zero, otherwise zero.
 */

static int 
grub_biosdisk_rw_int13_extensions (int ah, int drive, void *dap)
{
  struct grub_bios_int_registers regs;
  regs.eax = ah << 8;
  /* compute the address of disk_address_packet */
  regs.ds = (((grub_addr_t) dap) & 0xffff0000) >> 4;
  regs.esi = (((grub_addr_t) dap) & 0xffff);
  regs.edx = drive;
  regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;

  grub_bios_interrupt (0x13, &regs);
  return (regs.eax >> 8) & 0xff;
}

/*
 *   Call standard and old INT13 (int 13 %ah=AH) for DRIVE. Read/write
 *   NSEC sectors from COFF/HOFF/SOFF into SEGMENT. If an error occurs,
 *   return non-zero, otherwise zero.
 */
static int 
grub_biosdisk_rw_standard (int ah, int drive, int coff, int hoff,
			   int soff, int nsec, int segment)
{
  int ret, i;

  /* Try 3 times.  */
  for (i = 0; i < 3; i++)
    {
      struct grub_bios_int_registers regs;

      /* set up CHS information */
      /* set %ch to low eight bits of cylinder */
      regs.ecx = (coff << 8) & 0xff00;
      /* set bits 6-7 of %cl to high two bits of cylinder */
      regs.ecx |= (coff >> 2) & 0xc0;
      /* set bits 0-5 of %cl to sector */
      regs.ecx |= soff & 0x3f;

      /* set %dh to head and %dl to drive */  
      regs.edx = (drive & 0xff) | ((hoff << 8) & 0xff00);
      /* set %ah to AH */
      regs.eax = (ah << 8) & 0xff00;
      /* set %al to NSEC */
      regs.eax |= nsec & 0xff;

      regs.ebx = 0;
      regs.es = segment;

      regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;

      grub_bios_interrupt (0x13, &regs);
      /* check if successful */
      if (!(regs.flags & GRUB_CPU_INT_FLAGS_CARRY))
	return 0;

      /* save return value */
      ret = regs.eax >> 8;

      /* if fail, reset the disk system */
      regs.eax = 0;
      regs.edx = (drive & 0xff);
      regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
      grub_bios_interrupt (0x13, &regs);
    }
  return ret;
}

/*
 *   Check if LBA is supported for DRIVE. If it is supported, then return
 *   the major version of extensions, otherwise zero.
 */
static int
grub_biosdisk_check_int13_extensions (int drive)
{
  struct grub_bios_int_registers regs;

  regs.edx = drive & 0xff;
  regs.eax = 0x4100;
  regs.ebx = 0x55aa;
  regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
  grub_bios_interrupt (0x13, &regs);
  
  if (regs.flags & GRUB_CPU_INT_FLAGS_CARRY)
    return 0;

  if ((regs.ebx & 0xffff) != 0xaa55)
    return 0;

  /* check if AH=0x42 is supported */
  if (!(regs.ecx & 1))
    return 0;

  return (regs.eax >> 8) & 0xff;
}

/*
 *   Return the geometry of DRIVE in CYLINDERS, HEADS and SECTORS. If an
 *   error occurs, then return non-zero, otherwise zero.
 */
static int 
grub_biosdisk_get_diskinfo_standard (int drive,
				     unsigned long *cylinders,
				     unsigned long *heads,
				     unsigned long *sectors)
{
  struct grub_bios_int_registers regs;

  regs.eax = 0x0800;
  regs.edx = drive & 0xff;

  regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
  grub_bios_interrupt (0x13, &regs);

  /* Check if unsuccessful. Ignore return value if carry isn't set to 
     workaround some buggy BIOSes. */
  if ((regs.flags & GRUB_CPU_INT_FLAGS_CARRY) && ((regs.eax & 0xff00) != 0))
    return (regs.eax & 0xff00) >> 8;

  /* bogus BIOSes may not return an error number */  
  /* 0 sectors means no disk */
  if (!(regs.ecx & 0x3f))
    /* XXX 0x60 is one of the unused error numbers */
    return 0x60;

  /* the number of heads is counted from zero */
  *heads = ((regs.edx >> 8) & 0xff) + 1;
  *cylinders = (((regs.ecx >> 8) & 0xff) | ((regs.ecx << 2) & 0x0300)) + 1;
  *sectors = regs.ecx & 0x3f;
  return 0;
}

static int
grub_biosdisk_get_diskinfo_real (int drive, void *drp, grub_uint16_t ax)
{
  struct grub_bios_int_registers regs;

  regs.eax = ax;

  /* compute the address of drive parameters */
  regs.esi = ((grub_addr_t) drp) & 0xf;
  regs.ds = ((grub_addr_t) drp) >> 4;
  regs.edx = drive & 0xff;

  regs.flags = GRUB_CPU_INT_FLAGS_DEFAULT;
  grub_bios_interrupt (0x13, &regs);

  /* Check if unsuccessful. Ignore return value if carry isn't set to 
     workaround some buggy BIOSes. */
  if ((regs.flags & GRUB_CPU_INT_FLAGS_CARRY) && ((regs.eax & 0xff00) != 0))
    return (regs.eax & 0xff00) >> 8;

  return 0;
}

/*
 *   Return the cdrom information of DRIVE in CDRP. If an error occurs,
 *   then return non-zero, otherwise zero.
 */
static int
grub_biosdisk_get_cdinfo_int13_extensions (int drive, void *cdrp)
{
  return grub_biosdisk_get_diskinfo_real (drive, cdrp, 0x4b01);
}

/*
 *   Return the geometry of DRIVE in a drive parameters, DRP. If an error
 *   occurs, then return non-zero, otherwise zero.
 */
static int
grub_biosdisk_get_diskinfo_int13_extensions (int drive, void *drp)
{
  return grub_biosdisk_get_diskinfo_real (drive, drp, 0x4800);
}

static int
grub_biosdisk_get_drive (const char *name)
{
  unsigned long drive;

  if (name[0] == 'c' && name[1] == 'd' && name[2] == 0 && cd_drive)
    return cd_drive;

  if ((name[0] != 'f' && name[0] != 'h') || name[1] != 'd')
    goto fail;

  drive = grub_strtoul (name + 2, 0, 10);
  if (grub_errno != GRUB_ERR_NONE)
    goto fail;

  if (name[0] == 'h')
    drive += 0x80;

  return (int) drive ;

 fail:
  grub_error (GRUB_ERR_UNKNOWN_DEVICE, "not a biosdisk");
  return -1;
}

static int
grub_biosdisk_call_hook (grub_disk_dev_iterate_hook_t hook, void *hook_data,
			 int drive)
{
  char name[10];

  if (cd_drive && drive == cd_drive)
    return hook ("cd", hook_data);

  grub_snprintf (name, sizeof (name),
		 (drive & 0x80) ? "hd%d" : "fd%d", drive & (~0x80));
  return hook (name, hook_data);
}

static int
grub_biosdisk_iterate (grub_disk_dev_iterate_hook_t hook, void *hook_data,
		       grub_disk_pull_t pull)
{
  int num_floppies;
  int drive;

  /* For hard disks, attempt to read the MBR.  */
  switch (pull)
    {
    case GRUB_DISK_PULL_NONE:
      for (drive = 0x80; drive < 0x90; drive++)
	{
	  if (grub_biosdisk_rw_standard (0x02, drive, 0, 0, 1, 1,
					 GRUB_MEMORY_MACHINE_SCRATCH_SEG) != 0)
	    {
	      grub_dprintf ("disk", "Read error when probing drive 0x%2x\n", drive);
	      break;
	    }

	  if (grub_biosdisk_call_hook (hook, hook_data, drive))
	    return 1;
	}
      return 0;

    case GRUB_DISK_PULL_REMOVABLE:
      if (cd_drive)
	{
	  if (grub_biosdisk_call_hook (hook, hook_data, cd_drive))
	    return 1;
	}

      /* For floppy disks, we can get the number safely.  */
      num_floppies = grub_biosdisk_get_num_floppies ();
      for (drive = 0; drive < num_floppies; drive++)
	if (grub_biosdisk_call_hook (hook, hook_data, drive))
	  return 1;
      return 0;
    default:
      return 0;
    }
  return 0;
}

static grub_err_t
grub_biosdisk_open (const char *name, grub_disk_t disk)
{
  grub_uint64_t total_sectors = 0;
  int drive;
  struct grub_biosdisk_data *data;

  drive = grub_biosdisk_get_drive (name);
  if (drive < 0)
    return grub_errno;

  disk->id = drive;

  data = (struct grub_biosdisk_data *) grub_zalloc (sizeof (*data));
  if (! data)
    return grub_errno;

  data->drive = drive;

  if ((cd_drive) && (drive == cd_drive))
    {
      data->flags = GRUB_BIOSDISK_FLAG_LBA | GRUB_BIOSDISK_FLAG_CDROM;
      data->sectors = 8;
      disk->log_sector_size = 11;
      /* TODO: get the correct size.  */
      total_sectors = GRUB_DISK_SIZE_UNKNOWN;
    }
  else
    {
      /* HDD */
      int version;

      disk->log_sector_size = 9;

      version = grub_biosdisk_check_int13_extensions (drive);
      if (version)
	{
	  struct grub_biosdisk_drp *drp
	    = (struct grub_biosdisk_drp *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR;

	  /* Clear out the DRP.  */
	  grub_memset (drp, 0, sizeof (*drp));
	  drp->size = sizeof (*drp);
	  if (! grub_biosdisk_get_diskinfo_int13_extensions (drive, drp))
	    {
	      data->flags = GRUB_BIOSDISK_FLAG_LBA;

	      if (drp->total_sectors)
		total_sectors = drp->total_sectors;
	      else
                /* Some buggy BIOSes doesn't return the total sectors
                   correctly but returns zero. So if it is zero, compute
                   it by C/H/S returned by the LBA BIOS call.  */
                total_sectors = ((grub_uint64_t) drp->cylinders)
		  * drp->heads * drp->sectors;
	      if (drp->bytes_per_sector
		  && !(drp->bytes_per_sector & (drp->bytes_per_sector - 1))
		  && drp->bytes_per_sector >= 512
		  && drp->bytes_per_sector <= 16384)
		{
		  for (disk->log_sector_size = 0;
		       (1 << disk->log_sector_size) < drp->bytes_per_sector;
		       disk->log_sector_size++);
		}
	    }
	}
    }

  if (! (data->flags & GRUB_BIOSDISK_FLAG_CDROM))
    {
      if (grub_biosdisk_get_diskinfo_standard (drive,
					       &data->cylinders,
					       &data->heads,
					       &data->sectors) != 0)
        {
	  if (total_sectors && (data->flags & GRUB_BIOSDISK_FLAG_LBA))
	    {
	      data->sectors = 63;
	      data->heads = 255;
	      data->cylinders
		= grub_divmod64 (total_sectors
				 + data->heads * data->sectors - 1,
				 data->heads * data->sectors, 0);
	    }
	  else
	    {
	      grub_free (data);
	      return grub_error (GRUB_ERR_BAD_DEVICE, "%s cannot get C/H/S values", disk->name);
	    }
        }

      if (data->sectors == 0)
	data->sectors = 63;
      if (data->heads == 0)
	data->heads = 255;

      if (! total_sectors)
        total_sectors = ((grub_uint64_t) data->cylinders)
	  * data->heads * data->sectors;
    }

  disk->total_sectors = total_sectors;
  /* Limit the max to 0x7f because of Phoenix EDD.  */
  disk->max_agglomerate = 0x7f >> GRUB_DISK_CACHE_BITS;
  COMPILE_TIME_ASSERT ((0x7f >> GRUB_DISK_CACHE_BITS
			<< (GRUB_DISK_SECTOR_BITS + GRUB_DISK_CACHE_BITS))
		       + sizeof (struct grub_biosdisk_dap)
		       < GRUB_MEMORY_MACHINE_SCRATCH_SIZE);

  disk->data = data;

  return GRUB_ERR_NONE;
}

static void
grub_biosdisk_close (grub_disk_t disk)
{
  grub_free (disk->data);
}

/* For readability.  */
#define GRUB_BIOSDISK_READ	0
#define GRUB_BIOSDISK_WRITE	1

#define GRUB_BIOSDISK_CDROM_RETRY_COUNT 3

static grub_err_t
grub_biosdisk_rw (int cmd, grub_disk_t disk,
		  grub_disk_addr_t sector, grub_size_t size,
		  unsigned segment)
{
  struct grub_biosdisk_data *data = disk->data;

  /* VirtualBox fails with sectors above 2T on CDs.
     Since even BD-ROMS are never that big anyway, return error.  */
  if ((data->flags & GRUB_BIOSDISK_FLAG_CDROM)
      && (sector >> 32))
    return grub_error (GRUB_ERR_OUT_OF_RANGE,
		       N_("attempt to read or write outside of disk `%s'"),
		       disk->name);

  if (data->flags & GRUB_BIOSDISK_FLAG_LBA)
    {
      struct grub_biosdisk_dap *dap;

      dap = (struct grub_biosdisk_dap *) (GRUB_MEMORY_MACHINE_SCRATCH_ADDR
					  + (data->sectors
					     << disk->log_sector_size));
      dap->length = sizeof (*dap);
      dap->reserved = 0;
      dap->blocks = size;
      dap->buffer = segment << 16;	/* The format SEGMENT:ADDRESS.  */
      dap->block = sector;

      if (data->flags & GRUB_BIOSDISK_FLAG_CDROM)
        {
	  int i;

	  if (cmd)
	    return grub_error (GRUB_ERR_WRITE_ERROR, N_("cannot write to CD-ROM"));

	  for (i = 0; i < GRUB_BIOSDISK_CDROM_RETRY_COUNT; i++)
            if (! grub_biosdisk_rw_int13_extensions (0x42, data->drive, dap))
	      break;

	  if (i == GRUB_BIOSDISK_CDROM_RETRY_COUNT)
	    return grub_error (GRUB_ERR_READ_ERROR, N_("failure reading sector 0x%llx "
						       "from `%s'"),
			       (unsigned long long) sector,
			       disk->name);
	}
      else
        if (grub_biosdisk_rw_int13_extensions (cmd + 0x42, data->drive, dap))
	  {
	    /* Fall back to the CHS mode.  */
	    data->flags &= ~GRUB_BIOSDISK_FLAG_LBA;
	    disk->total_sectors = data->cylinders * data->heads * data->sectors;
	    return grub_biosdisk_rw (cmd, disk, sector, size, segment);
	  }
    }
  else
    {
      unsigned coff, hoff, soff;
      unsigned head;

      /* It is impossible to reach over 8064 MiB (a bit less than LBA24) with
	 the traditional CHS access.  */
      if (sector >
	  1024 /* cylinders */ *
	  256 /* heads */ *
	  63 /* spt */)
	return grub_error (GRUB_ERR_OUT_OF_RANGE,
			   N_("attempt to read or write outside of disk `%s'"),
			   disk->name);

      soff = ((grub_uint32_t) sector) % data->sectors + 1;
      head = ((grub_uint32_t) sector) / data->sectors;
      hoff = head % data->heads;
      coff = head / data->heads;

      if (coff >= data->cylinders)
	return grub_error (GRUB_ERR_OUT_OF_RANGE,
			   N_("attempt to read or write outside of disk `%s'"),
			   disk->name);

      if (grub_biosdisk_rw_standard (cmd + 0x02, data->drive,
				     coff, hoff, soff, size, segment))
	{
	  switch (cmd)
	    {
	    case GRUB_BIOSDISK_READ:
	      return grub_error (GRUB_ERR_READ_ERROR, N_("failure reading sector 0x%llx "
							 "from `%s'"),
				 (unsigned long long) sector,
				 disk->name);
	    case GRUB_BIOSDISK_WRITE:
	      return grub_error (GRUB_ERR_WRITE_ERROR, N_("failure writing sector 0x%llx "
							  "to `%s'"),
				 (unsigned long long) sector,
				 disk->name);
	    }
	}
    }

  return GRUB_ERR_NONE;
}

/* Return the number of sectors which can be read safely at a time.  */
static grub_size_t
get_safe_sectors (grub_disk_t disk, grub_disk_addr_t sector)
{
  grub_size_t size;
  grub_uint64_t offset;
  struct grub_biosdisk_data *data = disk->data;
  grub_uint32_t sectors = data->sectors;

  /* OFFSET = SECTOR % SECTORS */
  grub_divmod64 (sector, sectors, &offset);

  size = sectors - offset;

  return size;
}

static grub_err_t
grub_biosdisk_read (grub_disk_t disk, grub_disk_addr_t sector,
		    grub_size_t size, char *buf)
{
  while (size)
    {
      grub_size_t len;

      len = get_safe_sectors (disk, sector);

      if (len > size)
	len = size;

      if (grub_biosdisk_rw (GRUB_BIOSDISK_READ, disk, sector, len,
			    GRUB_MEMORY_MACHINE_SCRATCH_SEG))
	return grub_errno;

      grub_memcpy (buf, (void *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR,
		   len << disk->log_sector_size);

      buf += len << disk->log_sector_size;
      sector += len;
      size -= len;
    }

  return grub_errno;
}

static grub_err_t
grub_biosdisk_write (grub_disk_t disk, grub_disk_addr_t sector,
		     grub_size_t size, const char *buf)
{
  struct grub_biosdisk_data *data = disk->data;

  if (data->flags & GRUB_BIOSDISK_FLAG_CDROM)
    return grub_error (GRUB_ERR_IO, N_("cannot write to CD-ROM"));

  while (size)
    {
      grub_size_t len;

      len = get_safe_sectors (disk, sector);
      if (len > size)
	len = size;

      grub_memcpy ((void *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR, buf,
		   len << disk->log_sector_size);

      if (grub_biosdisk_rw (GRUB_BIOSDISK_WRITE, disk, sector, len,
			    GRUB_MEMORY_MACHINE_SCRATCH_SEG))
	return grub_errno;

      buf += len << disk->log_sector_size;
      sector += len;
      size -= len;
    }

  return grub_errno;
}

static struct grub_disk_dev grub_biosdisk_dev =
  {
    .name = "biosdisk",
    .id = GRUB_DISK_DEVICE_BIOSDISK_ID,
    .iterate = grub_biosdisk_iterate,
    .open = grub_biosdisk_open,
    .close = grub_biosdisk_close,
    .read = grub_biosdisk_read,
    .write = grub_biosdisk_write,
    .next = 0
  };

static void
grub_disk_biosdisk_fini (void)
{
  grub_disk_dev_unregister (&grub_biosdisk_dev);
}

GRUB_MOD_INIT(biosdisk)
{
  struct grub_biosdisk_cdrp *cdrp
    = (struct grub_biosdisk_cdrp *) GRUB_MEMORY_MACHINE_SCRATCH_ADDR;
  grub_uint8_t boot_drive;

  if (grub_disk_firmware_is_tainted)
    {
      grub_puts_ (N_("Native disk drivers are in use. "
		     "Refusing to use firmware disk interface."));
      return;
    }
  grub_disk_firmware_fini = grub_disk_biosdisk_fini;

  grub_memset (cdrp, 0, sizeof (*cdrp));
  cdrp->size = sizeof (*cdrp);
  cdrp->media_type = 0xFF;
  boot_drive = (grub_boot_device >> 24);
  if ((! grub_biosdisk_get_cdinfo_int13_extensions (boot_drive, cdrp))
      && ((cdrp->media_type & GRUB_BIOSDISK_CDTYPE_MASK)
	  == GRUB_BIOSDISK_CDTYPE_NO_EMUL))
    cd_drive = cdrp->drive_no;
  /* Since diskboot.S rejects devices over 0x90 it must be a CD booted with
     cdboot.S
   */
  if (boot_drive >= 0x90)
    cd_drive = boot_drive;

  grub_disk_dev_register (&grub_biosdisk_dev);
}

GRUB_MOD_FINI(biosdisk)
{
  grub_disk_biosdisk_fini ();
}