/////////////////////////////////////////////////////////////////////////
// $Id$
/////////////////////////////////////////////////////////////////////////
//
// Block driver and image creation code for
// Connectix / Microsoft Virtual PC images (ported from QEMU)
//
// Copyright (c) 2005  Alex Beregszaszi
// Copyright (c) 2009  Kevin Wolf <kwolf@suse.de>
// Copyright (C) 2012-2021  The Bochs Project
//
// 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.
//
/////////////////////////////////////////////////////////////////////////

// Define BX_PLUGGABLE in files that can be compiled into plugins.  For
// platforms that require a special tag on exported symbols, BX_PLUGGABLE
// is used to know when we are exporting symbols and when we are importing.
#define BX_PLUGGABLE

#ifdef BXIMAGE
#include "config.h"
#include "misc/bxcompat.h"
#include "osdep.h"
#include "misc/bswap.h"
#else
#include "bochs.h"
#include "plugin.h"
#endif
#include "hdimage.h"
#include "vpc.h"

#define LOG_THIS bx_hdimage_ctl.

#ifndef BXIMAGE

// disk image plugin entry point

PLUGIN_ENTRY_FOR_IMG_MODULE(vpc)
{
  if (mode == PLUGIN_PROBE) {
    return (int)PLUGTYPE_IMG;
  }
  return 0; // Success
}

#endif

//
// Define the static class that registers the derived device image class,
// and allocates one on request.
//
class bx_vpc_locator_c : public hdimage_locator_c {
public:
  bx_vpc_locator_c(void) : hdimage_locator_c("vpc") {}
protected:
  device_image_t *allocate(Bit64u disk_size, const char *journal) {
    return (new vpc_image_t());
  }
  int check_format(int fd, Bit64u disk_size) {
    return ((vpc_image_t::check_format(fd, disk_size) > 0) ? HDIMAGE_FORMAT_OK : -1);
  }
} bx_vpc_match;

Bit32u vpc_checksum(Bit8u *buf, size_t size)
{
  Bit32u res = 0;
  unsigned i;

  for (i = 0; i < size; i++)
    res += buf[i];

  return ~res;
}

int vpc_image_t::check_format(int fd, Bit64u imgsize)
{
  Bit8u temp_footer_buf[HEADER_SIZE];
  vhd_footer_t *footer;
  int vpc_disk_type = VHD_DYNAMIC;

  if (bx_read_image(fd, 0, (char*)temp_footer_buf, HEADER_SIZE) != HEADER_SIZE) {
    return HDIMAGE_READ_ERROR;
  }

  footer = (vhd_footer_t*)temp_footer_buf;
  if (strncmp((char*)footer->creator, "conectix", 8)) {
    if (imgsize < HEADER_SIZE) {
      return HDIMAGE_NO_SIGNATURE;
    }
    // If a fixed disk, the footer is found only at the end of the file
    if (bx_read_image(fd, imgsize-HEADER_SIZE, (char*)temp_footer_buf, HEADER_SIZE) != HEADER_SIZE) {
      return HDIMAGE_READ_ERROR;
    }
    if (strncmp((char*)footer->creator, "conectix", 8)) {
      return HDIMAGE_NO_SIGNATURE;
    }
    vpc_disk_type = VHD_FIXED;
  }
  return vpc_disk_type;
}

int vpc_image_t::open(const char* _pathname, int flags)
{
  int i;
  vhd_footer_t *footer;
  vhd_dyndisk_header_t *dyndisk_header;
  Bit8u buf[HEADER_SIZE];
  Bit32u checksum;
  Bit64u offset = 0, imgsize = 0;
  int disk_type;

  pathname = _pathname;
  if ((fd = hdimage_open_file(pathname, flags, &imgsize, &mtime)) < 0) {
    BX_ERROR(("VPC: cannot open hdimage file '%s'", pathname));
    return -1;
  }

  disk_type = check_format(fd, imgsize);
  if (disk_type < 0) {
    switch (disk_type) {
      case HDIMAGE_READ_ERROR:
        BX_ERROR(("VPC: cannot read image file header of '%s'", _pathname));
        return -1;
      case HDIMAGE_NO_SIGNATURE:
        BX_ERROR(("VPC: signature missed in file '%s'", _pathname));
        return -1;
    }
  } else if (disk_type == VHD_FIXED) {
    offset = imgsize - HEADER_SIZE;
  }
  if (bx_read_image(fd, offset, (char*)footer_buf, HEADER_SIZE) != HEADER_SIZE) {
    return -1;
  }
  footer = (vhd_footer_t*)footer_buf;

  checksum = be32_to_cpu(footer->checksum);
  footer->checksum = 0;
  if (vpc_checksum(footer_buf, HEADER_SIZE) != checksum) {
    BX_ERROR(("The header checksum of '%s' is incorrect", pathname));
    return -1;
  }

  // Write 'checksum' back to footer, or else will leave it with zero.
  footer->checksum = be32_to_cpu(checksum);

  // The visible size of a image in Virtual PC depends on the geometry
  // rather than on the size stored in the footer (the size in the footer
  // is too large usually)
  cylinders = be16_to_cpu(footer->cyls);
  heads = footer->heads;
  spt = footer->secs_per_cyl;
  sector_count = (Bit64u)((Bit64u)cylinders * heads * spt);
  sect_size = 512;

  /* Microsoft Virtual PC and Microsoft Hyper-V produce and read
   * VHD image sizes differently.  VPC will rely on CHS geometry,
   * while Hyper-V and disk2vhd use the size specified in the footer.
   *
   * We use a couple of approaches to try and determine the correct method:
   * look at the Creator App field, and look for images that have CHS
   * geometry that is the maximum value.
   *
   * If the CHS geometry is the maximum CHS geometry, then we assume that
   * the size is the footer->size to avoid truncation.  Otherwise,
   * we follow the table based on footer->creator_app:
   *
   *  Known creator apps:
   *      'vpc '  :  CHS              Virtual PC (uses disk geometry)
   *      'qemu'  :  CHS              QEMU (uses disk geometry)
   *      'qem2'  :  current_size     QEMU (uses current_size)
   *      'win '  :  current_size     Hyper-V
   *      'd2v '  :  current_size     Disk2vhd
   *      'tap\0' :  current_size     XenServer
   *      'CTXS'  :  current_size     XenConverter
   */
  bool use_chs = (!!strncmp(footer->creator_app, "win ", 4) &&
                  !!strncmp(footer->creator_app, "qem2", 4) &&
                  !!strncmp(footer->creator_app, "d2v ", 4) &&
                  !!strncmp(footer->creator_app, "CTXS", 4) &&
                  !!memcmp (footer->creator_app, "tap", 4));
  
  if (!use_chs || sector_count == (65535 * 16 * 255)) {
    sector_count = be64_to_cpu(footer->size) / sect_size;
  }
  hd_size = sector_count * sect_size;
  
  /* Allow a maximum disk size of 2040 GiB */
  if (sector_count > 0xff000000) {
    BX_ERROR(("VHD Emulated Image too large. " FMT_LL "i > 4278190080", sector_count));
    bx_close_image(fd, pathname);
    return -EFBIG;
  }

  if (disk_type == VHD_DYNAMIC) {
    if (bx_read_image(fd, be64_to_cpu(footer->data_offset), buf, HEADER_SIZE) != HEADER_SIZE) {
      bx_close_image(fd, pathname);
      return -1;
    }

    dyndisk_header = (vhd_dyndisk_header_t*)buf;

    if (strncmp((char*)dyndisk_header->magic, "cxsparse", 8)) {
      bx_close_image(fd, pathname);
      return -1;
    }

    block_size = be32_to_cpu(dyndisk_header->block_size);
    bitmap_size = ((block_size / (8 * 512)) + 511) & ~511;

    max_table_entries = be32_to_cpu(dyndisk_header->max_table_entries);
    pagetable = new Bit32u[max_table_entries];

    bat_offset = be64_to_cpu(dyndisk_header->table_offset);
    if (bx_read_image(fd, bat_offset, (void*)pagetable, max_table_entries * 4) != (max_table_entries * 4)) {
      bx_close_image(fd, pathname);
      return -1;
    }

    free_data_block_offset = (bat_offset + (max_table_entries * 4) + 511) & ~511;

    for (i = 0; i < max_table_entries; i++) {
      pagetable[i] = be32_to_cpu(pagetable[i]);
      if (pagetable[i] != 0xFFFFFFFF) {
        Bit64s next = (512 * (Bit64s)pagetable[i]) + bitmap_size + block_size;

        if (next > (Bit64s)free_data_block_offset) {
          free_data_block_offset = next;
        }
      }
    }

    last_bitmap_offset = (Bit64s) -1;
  }
  cur_sector = 0;

  BX_INFO(("'vpc' disk image opened: path is '%s'", pathname));

  return 0;
}

void vpc_image_t::close(void)
{
  if (fd > -1) {
    delete [] pagetable;
    bx_close_image(fd, pathname);
  }
}

Bit64s vpc_image_t::lseek(Bit64s offset, int whence)
{
  if (whence == SEEK_SET) {
    cur_sector = (Bit32u)(offset / 512);
  } else if (whence == SEEK_CUR) {
    cur_sector += (Bit32u)(offset / 512);
  } else {
    BX_ERROR(("lseek: mode not supported yet"));
    return -1;
  }
  if (cur_sector >= sector_count)
    return -1;
  return 0;
}

ssize_t vpc_image_t::read(void* buf, size_t count)
{
  char *cbuf = (char*)buf;
  Bit32u scount = (Bit32u)(count / 0x200);
  vhd_footer_t *footer = (vhd_footer_t*)footer_buf;
  Bit64s offset, sectors, sectors_per_block;
  int ret;

  if (cpu_to_be32(footer->type) == VHD_FIXED) {
    return bx_read_image(fd, cur_sector * 512, buf, count);
  }

  while (scount > 0) {
    offset = get_sector_offset(cur_sector, 0);

    sectors_per_block = block_size >> 9;
    sectors = sectors_per_block - (cur_sector % sectors_per_block);
    if (sectors > scount) {
      sectors = scount;
    }

    if (offset == -1) {
      memset(buf, 0, 512);
    } else {
      ret = bx_read_image(fd, offset, cbuf, (int)sectors * 512);
      if (ret != 512) {
        return -1;
      }
    }
    scount -= (Bit32u)sectors;
    cur_sector += sectors;
    cbuf += sectors * 512;
  }
  return count;
}

ssize_t vpc_image_t::write(const void* buf, size_t count)
{
  char *cbuf = (char*)buf;
  Bit32u scount = (Bit32u)(count / 512);
  vhd_footer_t *footer =  (vhd_footer_t*)footer_buf;
  Bit64s offset, sectors, sectors_per_block;
  int ret;

  if (cpu_to_be32(footer->type) == VHD_FIXED) {
    return bx_write_image(fd, cur_sector * 512, (void*)buf, count);
  }

  while (scount > 0) {
    offset = get_sector_offset(cur_sector, 1);

    sectors_per_block = block_size >> 9;
    sectors = sectors_per_block - (cur_sector % sectors_per_block);
    if (sectors > scount) {
      sectors = scount;
    }

    if (offset == -1) {
      offset = alloc_block(cur_sector);
      if (offset < 0)
        return -1;
    }

    ret = bx_write_image(fd, offset, cbuf, (int)sectors * 512);
    if (ret != sectors * 512) {
      return -1;
    }

    scount -= (Bit32u)sectors;
    cur_sector += sectors;
    cbuf += sectors * 512;
  }
  return count;
}

Bit32u vpc_image_t::get_capabilities(void)
{
  return HDIMAGE_HAS_GEOMETRY;
}

#ifdef BXIMAGE
int vpc_image_t::create_image(const char *pathname, Bit64u size)
{
  Bit8u buf[1024];
  Bit16u cyls = 0;
  Bit8u heads = 16, secs_per_cyl = 63;
  Bit64u total_sectors;
  Bit64s offset;
  size_t block_size, num_bat_entries;
  int fd, i;

  total_sectors = size >> 9;
  cyls = (Bit16u)(size/heads/secs_per_cyl/512.0);

  vhd_footer_t *footer = (vhd_footer_t*)buf;
  memset(footer, 0, HEADER_SIZE);
  memcpy(footer->creator, "conectix", 8);
  // TODO Check if "bxic" creator_app is ok for VPC
  memcpy(footer->creator_app, "bxic", 4);
  memcpy(footer->creator_os, "Wi2k", 4);

  footer->features = be32_to_cpu(0x02);
  footer->version = be32_to_cpu(0x00010000);
  footer->data_offset = be64_to_cpu(HEADER_SIZE);
  footer->timestamp = be32_to_cpu((Bit32u)time(NULL) - VHD_TIMESTAMP_BASE);

  // Version of Virtual PC 2007
  footer->major = be16_to_cpu(0x0005);
  footer->minor = be16_to_cpu(0x0003);
  footer->orig_size = be64_to_cpu(total_sectors * 512);
  footer->size = be64_to_cpu(total_sectors * 512);
  footer->cyls = be16_to_cpu(cyls);
  footer->heads = heads;
  footer->secs_per_cyl = secs_per_cyl;
  footer->type = be32_to_cpu(VHD_DYNAMIC);
  footer->checksum = be32_to_cpu(vpc_checksum(buf, HEADER_SIZE));

  fd = bx_create_image_file(pathname);
  if (fd < 0)
    BX_FATAL(("ERROR: failed to create vpc image file"));
  // Write the footer (twice: at the beginning and at the end)
  block_size = 0x200000;
  num_bat_entries = (size_t)(total_sectors + block_size / 512) / (block_size / 512);
  if (bx_write_image(fd, 0, footer, HEADER_SIZE) != HEADER_SIZE) {
    ::close(fd);
    BX_FATAL(("ERROR: The disk image is not complete - could not write footer!"));
  }
  offset = 1536 + ((num_bat_entries * 4 + 511) & ~511);
  if (bx_write_image(fd, offset, footer, HEADER_SIZE) != HEADER_SIZE) {
    ::close(fd);
    BX_FATAL(("ERROR: The disk image is not complete - could not write footer!"));
  }

  // Write the initial BAT
  memset(buf, 0xFF, 512);
  offset = 3 * 512;
  for (i = 0; i < (int)(num_bat_entries * 4 + 511) / 512; i++) {
    if (bx_write_image(fd, offset, buf, 512) != 512) {
      ::close(fd);
      BX_FATAL(("ERROR: The disk image is not complete - could not write BAT!"));
    }
    offset += 512;
  }

  vhd_dyndisk_header_t *header = (vhd_dyndisk_header_t*)buf;
  memset(header, 0, 1024);
  memcpy(header->magic, "cxsparse", 8);
  /*
   * Note: The spec is actually wrong here for data_offset, it says
   * 0xFFFFFFFF, but MS tools expect all 64 bits to be set.
   */
  header->data_offset = be64_to_cpu(0xFFFFFFFFFFFFFFFFULL);
  header->table_offset = be64_to_cpu(3 * 512);
  header->version = be32_to_cpu(0x00010000);
  header->block_size = be32_to_cpu(block_size);
  header->max_table_entries = be32_to_cpu(num_bat_entries);
  header->checksum = be32_to_cpu(vpc_checksum(buf, 1024));
  if (bx_write_image(fd, 512, header, 1024) != 1024) {
    ::close(fd);
    BX_FATAL(("ERROR: The disk image is not complete - could not write header!"));
  }

  ::close(fd);
  return 0;
}
#else
bool vpc_image_t::save_state(const char *backup_fname)
{
  return hdimage_backup_file(fd, backup_fname);
}

void vpc_image_t::restore_state(const char *backup_fname)
{
  int temp_fd;
  Bit64u imgsize;

  if ((temp_fd = hdimage_open_file(backup_fname, O_RDONLY, &imgsize, NULL)) < 0) {
    BX_PANIC(("cannot open vpc image backup '%s'", backup_fname));
    return;
  }

  if (check_format(temp_fd, imgsize) < HDIMAGE_FORMAT_OK) {
    ::close(temp_fd);
    BX_PANIC(("Could not detect vpc image header"));
    return;
  }
  ::close(temp_fd);
  close();
  if (!hdimage_copy_file(backup_fname, pathname)) {
    BX_PANIC(("Failed to restore vpc image '%s'", pathname));
    return;
  }
  device_image_t::open(pathname);
}
#endif

/*
 * Returns the absolute byte offset of the given sector in the image file.
 * If the sector is not allocated, -1 is returned instead.
 *
 * The parameter write must be 1 if the offset will be used for a write
 * operation (the block bitmaps is updated then), 0 otherwise.
 */
Bit64s vpc_image_t::get_sector_offset(Bit64s sector_num, int write)
{
  Bit64u offset = sector_num * 512;
  Bit64u bitmap_offset, block_offset;
  Bit32u pagetable_index, pageentry_index;

  pagetable_index = (Bit32u)(offset / block_size);
  pageentry_index = (offset % block_size) / 512;

  if ((pagetable_index >= (Bit32u)max_table_entries) ||
      (pagetable[pagetable_index] == 0xffffffff))
    return -1; // not allocated

  bitmap_offset = 512 * (Bit64u) pagetable[pagetable_index];
  block_offset = bitmap_offset + bitmap_size + (512 * pageentry_index);

  // We must ensure that we don't write to any sectors which are marked as
  // unused in the bitmap. We get away with setting all bits in the block
  // bitmap each time we write to a new block. This might cause Virtual PC to
  // miss sparse read optimization, but it's not a problem in terms of
  // correctness.
  if (write && (last_bitmap_offset != bitmap_offset)) {
    Bit8u *bitmap = new Bit8u[bitmap_size];

    last_bitmap_offset = bitmap_offset;
    memset(bitmap, 0xff, bitmap_size);
    bx_write_image(fd, bitmap_offset, bitmap, bitmap_size);
    delete [] bitmap;
  }

  return (Bit64s)block_offset;
}

/*
 * Writes the footer to the end of the image file. This is needed when the
 * file grows as it overwrites the old footer
 *
 * Returns 0 on success and < 0 on error
 */
int vpc_image_t::rewrite_footer()
{
  int ret;
  Bit64s offset = free_data_block_offset;

  ret = bx_write_image(fd, offset, footer_buf, HEADER_SIZE);
  if (ret < 0)
    return ret;

  return 0;
}

/*
 * Allocates a new block. This involves writing a new footer and updating
 * the Block Allocation Table to use the space at the old end of the image
 * file (overwriting the old footer)
 *
 * Returns the sectors' offset in the image file on success and < 0 on error
 */
Bit64s vpc_image_t::alloc_block(Bit64s sector_num)
{
  Bit64s new_bat_offset;
  Bit64u old_fdbo;
  Bit32u index, bat_value;
  int ret;

  // Check if sector_num is valid
  if ((sector_num < 0) || (sector_num > sector_count))
    return -1;

  // Write entry into in-memory BAT
  index = (Bit32u)((sector_num * 512) / block_size);
  if (pagetable[index] != 0xFFFFFFFF)
    return -1;

  pagetable[index] = (Bit32u)(free_data_block_offset / 512);

  // Initialize the block's bitmap
  Bit8u *bitmap = new Bit8u[bitmap_size];
  memset(bitmap, 0xff, bitmap_size);
  ret = bx_write_image(fd, free_data_block_offset, bitmap, bitmap_size);
  delete [] bitmap;
  if (ret < 0) {
    return ret;
  }

  // Write new footer (the old one will be overwritten)
  old_fdbo = free_data_block_offset;
  free_data_block_offset += (block_size + bitmap_size);
  ret = rewrite_footer();
  if (ret < 0) {
    free_data_block_offset = old_fdbo;
    return -1;
  }

  // Write BAT entry to disk
  new_bat_offset = bat_offset + (4 * index);
  bat_value = be32_to_cpu(pagetable[index]);
  ret = bx_write_image(fd, new_bat_offset, &bat_value, 4);
  if (ret < 0) {
    free_data_block_offset = old_fdbo;
    return -1;
  }

  return get_sector_offset(sector_num, 0);
}
