# Copyright 2019, 2021 VMware, Inc.
# All rights reserved.  -- VMware Confidential

"""Legacy BIOS boot support via Syslinux

When the world moves away from BIOS and instead uses UEFI for booting ESX,
this can then become obsolete and vanish into thin air. For the time being,
it sticks around and locked onto Syslinux 3.8x.
"""
import os
import shutil
import struct
import logging

from systemStorage import *
from systemStorage.vfat import (Fat16Partition, FatVolumeBootRecord, bytesToLE,
                                mcopy)

# Supported version of ldlinux.sys must be <= this number of sectors
MAX_LDLINUX_SECTORS = 64

# Magic number in ldlinux to commence patching
LDLINUX_MAGIC_INT = 0x3EB202FE
LDLINUX_MAGIC = b'\xfe\x02\xb2\x3e'

# Offset in partition's boot sector which points to ldlinux first sector
BOOTSECT_LDLINUX_SECTOR_PATCH_OFFSET = 0x1F8

log = logging.getLogger(os.path.basename(__file__))
log.setLevel(logging.DEBUG)

def set32bitsLE(buf, offset, value):
   """Store 32bit value in buffer at offset in little endian format
   """
   struct.pack_into('<I', buf, offset, value)

class SyslinuxPartition(Fat16Partition):
   """BIOS bootable FAT16 partition handled by Syslinux.

   The patching involves:
    1 Identifying the location of LDLINUX.SYS is in the partition
    2 Collecting all its sectors
    3 Store its first sector location into the volume boot record
    4 Write its size in words and number of sectors immediately after the magic
    5 Write its sector-chain in the block immediately after that
    6 Calculate the entire LDLINUX.SYS checksum
    7 Store the checksum after the the sectors written at #3
    8 Write the newly patched volume boot record to disk
    9 Write the first sector of the patched LDLINUX.SYS to disk

   Example use of this class:
     f = SyslinuxPartition("/path/to/fat-partition")
     f.patchSyslinux("new-fat-boot-sect")
   """

   def __init__(self, diskPath, partitionOffset=0, sectorSize=512, logger=None):
      super().__init__(diskPath, partitionOffset, sectorSize, logger)

   def _patchLdlinux(self, ldlinux, ldlinuxSectors):
      """Patch the ldlinux buffer with the sector chain in ldlinuxSectors.
      """
      patchOffset = ldlinux.find(LDLINUX_MAGIC, 0, self.sectorSize)
      assert patchOffset != -1, "invalid ldlinux.sys (magic not found)"

      # skip 2 words used to store file information and csum
      patchOffset += 8

      fileSizeWords = len(ldlinux) // 4        # size of file in 4-byte words
      sectors = len(ldlinuxSectors)            # sectors to traverse
      patchVal = (sectors << 16) | fileSizeWords

      self.debug("Patching csum header: 0x%04x, sectors %d, filesize-words %d" %
                 (patchVal, sectors, fileSizeWords))

      set32bitsLE(ldlinux, patchOffset, patchVal)

      # Patch the sectors chain
      p = patchOffset + 8
      for sector in ldlinuxSectors:
         set32bitsLE(ldlinux, p, sector)
         p += 4

      # Erase checksum area, and calculate the new ldlinux checksum
      set32bitsLE(ldlinux, patchOffset + 4, 0)

      checksum = int(LDLINUX_MAGIC_INT)
      for i in range(0, len(ldlinux) // 4):
         v = bytesToLE(ldlinux[i * 4: i * 4 + 4])
         checksum = (checksum - v) & 0xffffffff

      self.debug("LDLINUX.SYS checksum 0x%x" % checksum)

      set32bitsLE(ldlinux, patchOffset + 4, checksum)

   def _patchBootSector(self, currBootSect, volBootSectorFile, ldlinuxSector):
      """Fixup the Syslinux boot sector.

      Patch the current boot sector with the contents of the new volume boot
      sector and the ldlinux sector, but preserving the partition's FAT boot
      parameter block which describes the layout of the filesystem.

      Return the newly patched boot sector of the partition.
      """
      with open(volBootSectorFile, 'rb') as f:
         bootSect = bytearray(f.read(FatVolumeBootRecord.BOOT_SECTOR_SIZE))

      # Update the location of ldlinux.sys in the boot sector
      set32bitsLE(bootSect, BOOTSECT_LDLINUX_SECTOR_PATCH_OFFSET,
                  ldlinuxSector)

      # Preserve the boot sector's FAT boot parameter block
      preservedBootSect = currBootSect[FatVolumeBootRecord.JUMP_INSTR_LEN :
                                       FatVolumeBootRecord.FAT16_CODE_OFFSET]

      newBootSect = (bootSect[0:FatVolumeBootRecord.JUMP_INSTR_LEN] +
                     preservedBootSect +
                     bootSect[FatVolumeBootRecord.FAT16_CODE_OFFSET:])

      return newBootSect

   def _doPatchSyslinux(self, volBootSectorFile, ldlinuxPath, doWrite=False):
      """Patch syslinux associated sectors residing in this FAT partition.

      The BIOS parameter block of this partition is preserved as it describes
      the layout of the partition. Only the boot code is updated from the
      provided volBootSectorFile contents.

      @param volBootSectorFile: filename of image containing the volume boot
                                sector to copy to the partition; it needs to
                                have the boot code installed.
      @param ldlinuxPath:       path to ldlinux.sys
      @param doWrite:           write over the sectors in this partition.

      Prerequisite: LDLINUX.SYS must have already been copied to the partition.
      """
      currBootSect = self.readBootRecord()

      # Patch the bootsector with the first sector of ldlinux
      ldlinuxSectors, ldlinux = self.getFileSectors(os.path.basename(ldlinuxPath))

      assert len(ldlinuxSectors) <= (MAX_LDLINUX_SECTORS + 1), \
         ("%s: invalid ldlinux.sys (too big: %u > %u)" %
          (ldlinuxPath, len(ldlinuxSectors) * self.sectorSize,
           (MAX_LDLINUX_SECTORS + 1) * self.sectorSize))

      newBootSect = self._patchBootSector(currBootSect, volBootSectorFile,
                                          ldlinuxSectors[0])

      ldlinux = bytearray(ldlinux)

      # When patching ldlinux, skip the first sector because it's
      # not part of the sector chain used for calculating the checksum
      self._patchLdlinux(ldlinux, ldlinuxSectors[1:])

      if doWrite:
         # Write to boot sector and ldlinux first sector directly
         self.writeSectors(0, newBootSect)
         self.writeSectors(ldlinuxSectors[0], bytes(ldlinux)[:self.sectorSize])
      else:
         # Write sectors to files in temporary directory for user inspection
         fn = os.path.join(os.path.sep, "tmp",
                           os.path.basename(volBootSectorFile) + ".new")
         with open(fn, "wb+") as f:
            f.write(newBootSect)
         self.debug("Wrote new boot sector file %s" % fn)

         fn = os.path.join(os.path.sep, "tmp", "ldlinux.sys.sector0")
         with open(fn, "wb+") as f:
            f.write(bytes(ldlinux)[:self.sectorSize])
         self.debug("Wrote ldlinux.sys sector-0 file %s" % fn)

   def patchSyslinux(self, volBootSectorFile, ldlinuxPath, doWrite=True):
      """Patch the partition's syslinux sectors.

      This is the wrapper to the worker method, handling opening and closing of
      the partition image.
      """
      try:
         self.open(write=doWrite)
         self._doPatchSyslinux(volBootSectorFile, ldlinuxPath, doWrite)
      finally:
         self.close()

def installBootSector(devPath, bootCodePath):
   """Install boot sector on a disk.
   """
   bootCodeSize = os.path.getsize(bootCodePath)

   log.debug('Flush disk boot sector with %s, size %u',
             bootCodePath, bootCodeSize)

   writtenBytes = 0
   with open(bootCodePath, 'rb') as srcFile:
      with open(devPath, 'rb+') as destFile:
         buf = srcFile.read()
         assert len(buf) == bootCodeSize, ("%s: incomplete read (%u/%u bytes) "
                                           "while installing syslinux boot code"
                                           % (bootCodePath, len(buf),
                                              bootCodeSize))
         writtenBytes = destFile.write(buf)

   assert writtenBytes == bootCodeSize, ("%s: incomplete write (%u/%u bytes) "
                                         "while installing sylinux on disk" %
                                         (disk.name, writtenBytes,
                                          bootCodeSize))

def patchBootPartition(devPath, bootPartOffset, sectorSize, fatBootSectorPath,
                       ldlinuxPath, mcopyBin=None):
   """Patch the boot partition to be BIOS bootable with syslinux files.

   The process involves:
     1 Copying ldlinux.sys to the boot partition
     2 Patching the boot sector and ldlinux.sys in the boot partition

   Note that this does not upgrade the disk's boot sector which requires
   either mbr.bin or gptmbr.bin to be copied to it.
   """
   for path in (fatBootSectorPath, ldlinuxPath):
      assert os.path.exists(path), "%s: file not found" % path

   # Copy ldlinux to boot partition
   linuxDestPath = os.path.basename(ldlinuxPath)
   byteOffset = bootPartOffset * sectorSize
   mcopy(devPath, [ldlinuxPath], linuxDestPath, byteOffset=byteOffset,
         exe=mcopyBin)

   # Patch the boot partition
   # Identify the sector offset of the partition in the disk device
   fatPatcher = SyslinuxPartition(devPath, partitionOffset=bootPartOffset)
   fatPatcher.patchSyslinux(fatBootSectorPath, linuxDestPath, doWrite=True)

   log.info("Patched boot partition successfully")


if __name__ == "__main__":
   """Test code
   For verification of this module:

   dd if=/dev/zero of=fat.dd bs=1M count=100
   mkdosfs -F 16 fat.dd
   syslinux -s fat.dd

   # extract boot sector
   dd if=fat.dd of=fat_boot_sector bs=512 count=1
   # extract ldlinux.sys - so that we can do a diff after patching
   mcopy -i fat.dd ::ldlinux.sys ldlinux.sys

   # run this script, it will create /tmp/ldlinux.sys.sector0 and
   # /tmp/new_fat_boot_sector.new
   python3 <thisfile>.py fat.dd new_fat_boot_sector

   # verify that the patching works
   dd if=ldlinux.sys of=ldlinux.sys.0 bs=512 count=1
   diff ldlinux.sys.0 ldlinux.sys.sector0
   diff fat_boot_sector fat_boot_sector.new

   ---
   In a real setup, we distribute ldlinux.sys and fat_boot_sector files.
   Then we copy ldlinux.sys to the FAT partition.
   Then pathSyslinux would be called to patch the bootsector with the
   new_fat_boot_sector, and ldlinux.sys that's just been copied.
   """
   import sys
   import getopt
   import logging
   from esxLogging import setupLogger

   try:
      optlist, args = getopt.getopt(sys.argv, "w")
   except getopt.GetoptError as err:
      print (err)
      sys.exit(1)

   if len(args) < 3:
      print("Usage: syslinux.py [-w] ddimage boot_sector_file")
      sys.exit(1)
   doWrite = False
   for o, _ in optlist:
      if o == "-w":
         doWrite = True
   f = SyslinuxPartition(args[1], logger=setupLogger("syslinux", logging.DEBUG))
   f.patchSyslinux(args[2], 'ldlinux.sys', doWrite=doWrite)
