# Copyright (c) 2018-2019, 2021-2023 VMware, Inc.  All rights reserved.
# -- VMware Confidential

"""Python bindings for the libvfat.so library and vfat support functions.

A FAT partition is designed with the following layout (the terminology
used in this module is based on this information):

   SECTOR   CONTENTS
          +-------------------------------------------------------+
       0  | Volume boot record (VBR) - metadata and boot code     |
          +-------------------------------------------------------+
       1  | Reserved blocks                                       |
          |   size = VBR.reservedSectors                          |
          +-------------------------------------------------------+
          | File allocation table (cluster map)                   |
          |   size = VBR.numFATs * sectorsPerFAT                  |
          +-------------------------------------------------------+
          | Root directory block                                  |
          |   size = (VBR.maxRootEntries * 32)/sectorSize         |
          +-------------------------------------------------------+
          | Data region - where file data and directory data      |
          | reside.                                               |
          |                                                       |
      end +.......................................................+

"""
from ctypes import *
import math
import os
import struct
import subprocess

from borautils.dl import SharedLibrary

from systemStorage import (IS_ESX, IS_SYSTEM_STORAGE_NEXT_ENABLED,
                           VFAT_MAX_SECTORS)
from systemStorage.esxfs import EsxFsUuid, SIZEOF_FS_UUID
from systemStorage.blockdev import BlockDev

VMWARE_VFAT_MAGIC = b"VMWARE FAT16    "

def bytesToLE(buf):
   """Convert bytes to little endian
   """
   if len(buf) == 8:
      res = struct.unpack('<Q', buf)
   elif len(buf) == 4:
      res = struct.unpack('<I', buf)
   elif len(buf) == 2:
      res = struct.unpack('<H', buf)
   else:
      raise ValueError("Invalid buffer length")
   return res[0]

def isVfatPartition(partitionPath, sectorSize):
   """Verify if the partition is a VMware FAT partition.

   In legacy ESXi, the boot partition is formatted with ESXi...FAT16 labels.
   """
   volume = BlockDev(partitionPath, sectorSize, 0, isRegularFile=True)
   volume.open(os.O_RDONLY)
   try:
      buf = volume.readBlock(0, 2)
      bootRec = FatVolumeBootRecord(buf)
   except ValueError:
      # Invalid FAT volume, the partition might be unformatted.
      return False
   finally:
      volume.close()

   return ((bootRec.volumeLabel == "ESXi" and bootRec.systemId == "FAT16") or
       buf[sectorSize:sectorSize + len(VMWARE_VFAT_MAGIC)] == VMWARE_VFAT_MAGIC)

class FatVolumeBootRecord(object):
   """A class to represent a FAT Boot Record sector.

   The Boot Record is the first sector of a FAT partition, which contains the
   BIOS parameter block and other volume metadata.

   The boot code block is not cached here as it generated by bootloader
   installers, such as syslinux.
   """
   BOOT_SECTOR_SIZE = 512      # total size of boot record sector
   BOOT_RECORD_LEN = 62        # size of the relevant boot record fields
   JUMP_INSTR_LEN = 3          # the size of the first jump instruction
   FAT16_CODE_OFFSET = 0x3E    # offset within VBR where code starts
   _FMT = '=3s8sHBHBHHBHHHLLBBBL11s8s'

   def __init__(self, sectorBuf):
      """Initialize instance with the provided sector bytes.
      """
      (self.jmpInstr,
       self.oemName,
       self.bytesPerSector,
       self.sectorsPerCluster,
       self.reservedSectors,
       self.numFATs,
       self.maxRootEntries,
       self.sectorsSmall,
       self.mediaType,
       self.sectorsPerFAT,
       self.sectorsPerTrack,
       self.numHeads,
       self.numHiddenSectors,
       self.sectorsLarge,
       self.physDiskNumber,
       self.currentHead,
       self.signature,
       self.serialNumber,
       self.volumeLabel,
       self.systemId) = struct.unpack(self._FMT,
                                      sectorBuf[:self.BOOT_RECORD_LEN])

      if self.bytesPerSector == 0:
         # Catch unformatted zeroed partition before we try to divide by 0.
         raise ValueError('Invalid FAT volume: sector size is 0')

      # Offsets and sizes of different regions in the partition
      self.fatStartSector = self.reservedSectors
      self.rootDirectoryStartSector = (self.fatStartSector +
                                       self.numFATs * self.sectorsPerFAT)
      self.rootDirSectors = math.ceil(self.maxRootEntries *
                                      FatDirectoryEntry.ENTRY_LEN /
                                      self.bytesPerSector)
      self.clusterBytes = self.bytesPerSector * self.sectorsPerCluster

      try:
         self.volumeLabel = self.volumeLabel.decode('ascii').strip('\x00 ')
         self.systemId = self.systemId.decode('ascii').strip('\x00 ')
      except UnicodeDecodeError:
         self.volumeLabel = ""
         self.systemId = ""

      self.dataStartSector = self.rootDirectoryStartSector + self.rootDirSectors

   def __repr__(self):
      return ("JMP:                  %s,\n"
              "OEM Name:             %s,\n"
              "Bytes per sector:     %u,\n"
              "Sectors per cluster:  %u,\n"
              "Rsvd sector count:    %u,\n"
              "Number of FATs:       %u,\n"
              "Max root entries:     %u,\n"
              "Total sectors (sm):   %u,\n"
              "Media descriptor:     %s,\n"
              "Sectors per FAT:      %u,\n"
              "Sectors per track:    %u,\n"
              "Number of heads:      %u,\n"
              "Hidden sectors:       %u,\n"
              "Total sectors (lg):   %u,\n"
              "Volume label:         %s,\n"
              "System ID:            %s,\n"
              "Root dir sector:      %#x\n") % (
              self.jmpInstr,
              self.oemName,
              self.bytesPerSector,
              self.sectorsPerCluster,
              self.reservedSectors,
              self.numFATs,
              self.maxRootEntries,
              self.sectorsSmall,
              self.mediaType,
              self.sectorsPerFAT,
              self.sectorsPerTrack,
              self.numHeads,
              self.numHiddenSectors,
              self.sectorsLarge,
              self.volumeLabel,
              self.systemId,
              self.rootDirectoryStartSector)

class FatDirectoryEntry(object):
   """A class to store a FAT directory entry.

   TODO: support long filename
   """
   # Entry attributes
   READONLY = 0x01
   HIDDEN = 0x02
   SYSTEM = 0x04
   LABEL = 0x08
   DIRECTORY = 0x10
   ARCHIVE = 0x20
   LONGFILENAME = READONLY | HIDDEN | SYSTEM | LABEL

   ENTRY_LEN = 32
   _FMT = "<8s3sBBBHHHHHHHI"
   _LFN_FMT = "<B10sBBB12sH4s"

   def __init__(self, dirEntryBuf):
      """Initialize instance from the provided directory entry buffer
      """
      (self.fileName,
       self.fileExtension,
       self.attributes,
       self.reserved,
       self.createTimeFine,
       self.createTime,
       self.createDate,
       self.lastAccessDate,
       self.extendedAttributes,
       self.lastModifiedTime,
       self.lastModifiedDate,
       self.startOfFileClusters,
       self.fileSize) = struct.unpack(self._FMT, dirEntryBuf)

      if self.isLongFilename:
         (self.lfnSeq,
          self.lfnName1,
          self.lfnAttributes,
          self.lfnType,
          self.lfnChecksum,
          self.lfnName2,
          self.lfnFirstCluster,
          self.lfnName3) = struct.unpack(self._LFN_FMT, dirEntryBuf)

         self.fullFileName = ""
      else:
         if not isinstance(self.fileName, str):
            try:
               self.fileName = self.fileName.decode('utf-8')
               self.fileExtension = self.fileExtension.decode('utf-8')
            except UnicodeDecodeError:
               self.fileName = ""
               self.fileExtension = ""
         self.fileName = self.fileName.strip('\x00 ')
         self.fileExtension = self.fileExtension.strip('\x00 ')

         if self.fileExtension:
            self.fullFileName = self.fileName + '.' + self.fileExtension
         else:
            self.fullFileName = self.fileName
         self.shortFileName = self.fullFileName.upper()

   def __eq__(self, other):
      """Compare if filenames are the same or instances are the same
      """
      if type(other) is str:
         otherUpper = other.upper()
         return (otherUpper == self.fullFileName.upper() or
                 otherUpper == self.shortFileName)
      elif isinstance(other, self.__class__):
         return self.__dict__ == other.__dict__
      return False

   def __repr__(self):
      return ("File name:          %s\n"
              "File extension:     %s\n"
              "Full name:          %s\n"
              "Attributes:         %#x\n"
              "Reserved:           %#x\n"
              "Create time (fine): %#x\n"
              "Create time:        %#x\n"
              "Create date:        %#x\n"
              "Last access date:   %#x\n"
              "Extended attribs:   %#x\n"
              "Last modified time: %#x\n"
              "Last modified date: %#x\n"
              "Starting cluster:   %#x\n"
              "File size:          %u\n") % (
              self.fileName,
              self.fileExtension,
              self.fullFileName,
              self.attributes,
              self.reserved,
              self.createTimeFine,
              self.createTime,
              self.createDate,
              self.lastAccessDate,
              self.extendedAttributes,
              self.lastModifiedTime,
              self.lastModifiedDate,
              self.startOfFileClusters,
              self.fileSize)

   @property
   def isFree(self):
      """Returns True if entry is free (ie unused or deleted).
      """
      return self.isLast or self.fileName[0] in ('\x05', '\xe5')

   @property
   def isLast(self):
      """True if this is the terminating entry in the directory entries table.
      """
      return len(self.fileName) == 0 or self.fileName[0] == '\x00'

   @property
   def isDir(self):
      """True if entry is a directory.
      """
      return (self.attributes & self.DIRECTORY) != 0

   @property
   def isLabel(self):
      """True if entry is a label (not file or dir).
      """
      return (self.attributes & self.LABEL) != 0

   @property
   def isLongFilename(self):
      return (self.attributes & self.LONGFILENAME) == self.LONGFILENAME

   @classmethod
   def fromBuffer(cls, dirEntriesBuf):
      """Construct class from the provided directory entries table.

      Long filename entries exist as a contiguous set of entries, where
      the long-filename entry types appear before the actual directory entry,
      i.e.
          long-filename-entry-n
          long-filename-entry-2
          long-filename-entry-1
          actual-dir-entry

      There can be up to 20 such entries for a single file.

      This method squashes all the LFN entries and the actual dir-entry into
      one object.

      Return a tuple of:
         (FatDirectoryEntry object,
          number of dirtable-entries consumed to obtain this directory entry)
      """

      dirEntries = []

      # iterate through each entry and combine related entries
      for i in range(0, len(dirEntriesBuf) // cls.ENTRY_LEN):
         start = i * cls.ENTRY_LEN
         entry = FatDirectoryEntry(dirEntriesBuf[start : start+cls.ENTRY_LEN])
         dirEntries.append(entry)

         if entry.isFree:
            # This entry is free (deleted or terminator), any preceding LFN
            # ones are invalid.
            return (entry, i + 1)

         if not entry.isLongFilename:
            # A short filename entry is the last one in the list.
            break

      # The short filename entry (or last one in the table)
      dirEntry = dirEntries[-1]

      if len(dirEntries) == 1:
         # Only a short filename entry exists
         return (dirEntry, len(dirEntries))

      # Concat the LFN entries to generate the long filename string.
      dirEntries = sorted(dirEntries[:-1], key=lambda x: x.lfnSeq)
      fileName = b''
      expectedSeq = 1
      skipLfn = False
      for entry in dirEntries:
         if (entry.lfnSeq & 0x1F) != expectedSeq:
            # Invalid filename sequence, ignore them all.
            skipLfn = True
            break

         expectedSeq += 1
         fileName = fileName + entry.lfnName1 + entry.lfnName2 + entry.lfnName3

      if not skipLfn:
         try:
            # Decode 16-bit wide characters and strip out erroneous characters
            dirEntry.fullFileName = (fileName.decode('utf-16')
                                             .strip('\u0000\uffff\x00\xff'))
         except UnicodeDecodeError:
            dirEntry.fullFileName = ""

      return (dirEntry, len(dirEntries) + 1)


class FatPartition(object):
   """Class to represent a FAT partition.

   Common FAT class which implements methods for reading from the root
   directory and writing to sectors in the partition.
   """

   def __init__(self, diskPath, fatBits=16, partitionOffset=0, sectorSize=512,
                logger=None):
      if fatBits == 16:
         self.bytesPerFATEntry = 2
         self.maxClusters = 64 * 1024 - 1
         self.endOfChainIdent = 0xFFFF
      elif fatBits == 32:
         self.bytesPerFATEntry = 4
         self.maxClusters = 4177918
         self.endOfChainIdent = 0xFFFFFF
      else:
         raise ValueError("Unsupported FAT bits %s" % fatBits)

      self.diskPath = diskPath
      self.partitionOffset = partitionOffset * sectorSize
      self.sectorSize = sectorSize
      self.fileHandler = None
      self.vbr = None
      self.logger = logger

   def debug(self, message):
      if self.logger:
         self.logger.debug(message)

   def open(self, write=False):
      if self.fileHandler is None:
         self.fileHandler = open(self.diskPath, 'rb+' if write else 'rb')

   def setFileHandler(self, handler):
      assert self.fileHandler is None
      self.fileHandler = handler

   def close(self):
      if self.fileHandler is not None:
         self.fileHandler.close()
         self.fileHandler = None

   def seek(self, offset):
      """Seek to position in the filehandler.
      """
      self.fileHandler.seek(self.partitionOffset + offset)

   def readSectors(self, sector, count):
      """Read sectors from partition at given sector offset.
      """
      self.seek(sector * self.sectorSize)
      return self.fileHandler.read(count * self.sectorSize)

   def writeSectors(self, sector, buf):
      """Write the buffer at the specified sector.
      """
      self.seek(sector * self.sectorSize)
      return self.fileHandler.write(buf)

   def readBootRecord(self):
      """Reads the partition's boot sector.
      """
      buf = self.readSectors(0, 1)
      self.vbr = FatVolumeBootRecord(buf)
      self.sectorSize = self.vbr.bytesPerSector
      return buf

   def readCluster(self, cluster):
      """Read the cluster from the partition.
      """
      if self.vbr is None:
         self.readBootRecord()
      startSect = (self.vbr.dataStartSector +
                   cluster * self.vbr.sectorsPerCluster)
      return self.readSectors(startSect, self.vbr.sectorsPerCluster)

   def iterDirectoryEntries(self, entriesTable, maxEntries, matchName=None):
      """Walk the directory table entries.

      If matchName is specified, then yield only on matching the name, otherwise
      yield on all entries.
      """
      i = 0
      while i < maxEntries:
         start = i * FatDirectoryEntry.ENTRY_LEN
         entry, n = FatDirectoryEntry.fromBuffer(entriesTable[start:])

         # Skip consumed entries for next round
         i += n

         if entry.isLast:
            break

         if entry.isFree:
            continue

         if matchName is None or entry == matchName:
            yield entry

            if matchName is not None:
               # Done if matched.
               return

      if matchName is not None:
         raise FileNotFoundError("Directory entry %s not found" % matchName)

   def walkRootDirectory(self, matchName=None):
      """Walk the root directory.

      If matchName is specified, then yield only on entries which match the
      specified name, otherwise yield on all entries.

      @param matchName: filename to match
      """
      startSector = self.vbr.rootDirectoryStartSector
      rootDirEntriesRaw = self.readSectors(startSector, self.vbr.rootDirSectors)
      return self.iterDirectoryEntries(rootDirEntriesRaw,
                                       self.vbr.maxRootEntries, matchName)

   def getClusterChain(self, startCluster):
      """Collect the cluster chain beginning with the starting cluster.
      """
      data = self.readSectors(self.vbr.fatStartSector,
                    self.vbr.numFATs * self.vbr.sectorsPerFAT)

      # Every cluster index retrieved must subtract 2 because the first two
      # entries in the FAT cluster map are reserved.
      clusters = [startCluster - 2]
      while len(clusters) < self.maxClusters:
         v = data[startCluster * 2 : startCluster * 2 + self.bytesPerFATEntry]
         if isinstance(v[0], str):
            startCluster = bytesToLE([ord(i) for i in v])
         else:
            startCluster = bytesToLE(v)
         if startCluster == self.endOfChainIdent:
            break
         clusters.append(startCluster - 2)
      if startCluster != self.endOfChainIdent:
         # The loop should have been broken by reaching an end of chain marker.
         # If it's not there, then there's an overflow due to FAT chain
         # corruption.
         raise OverflowError("FAT chain corruption detected")
      return clusters

   def getDirEntry(self, path):
      """Walk the given directory tree and retrieve its directory entry.

      @param path: an array of strings or path-string.
      Returns a FatDirectoryEntry for the basename (last) field of the path.
      """
      if isinstance(path, list):
         fullPath = os.path.join(os.path.sep, *path)
      else:
         fullPath = path
         path = path.split(os.path.sep)

      if path[0] == '':
         path = path[1:]

      # Get root directory entry
      try:
         dirEntries = list(self.walkRootDirectory(path[0]))
      except FileNotFoundError:
         raise FileNotFoundError("Path %s not found" % fullPath)

      dirEntry = dirEntries[0]

      # Entry is in root
      if len(path) == 1:
         return dirEntry

      path = path[1:]
      while len(path) > 0:
         if path[0] == '':
            path = path[1:]
            continue

         # While multiple items still in path, entry must be a directory type
         if len(path) > 1 and not dirEntry.isDir:
            raise FileNotFoundError("%s does not exist" % fullPath)

         if dirEntry.startOfFileClusters == 0:
            # Jump back to root directory
            return self.getDirEntry(path)
         else:
            _, data = self.getDirEntrySectors(dirEntry,
                                              doBuffering=dirEntry.isDir)
            maxEntries = len(data) // FatDirectoryEntry.ENTRY_LEN
            entries = list(self.iterDirectoryEntries(data, maxEntries, path[0]))
            if len(entries) == 0:
               raise FileNotFoundError("%s does not exist" % fullPath)

         dirEntry = entries[0]
         path = path[1:]

      return dirEntry

   def getDirEntrySectors(self, dirEntry, outputStream=None, doBuffering=True):
      """Retrieve the sectors belonging to the specified directory entry.

      @param dirEntry: FatDirectoryEntry directory entry to fetch sectors for
      @param outputStream: write file output to object instead of
                           storing it in a memory buffer.
      @param doBuffering: buffer data into memory

      Return a tuple of ([]dirEntry-sector-numbers, bytes of entry data)
      """
      entrySectors = []
      buf = b''

      # Get cluster chain which defines which sectors to read in the partition
      chain = self.getClusterChain(dirEntry.startOfFileClusters)

      # Directory tables don't have a size defined; its data is defined from
      # the cluster chain.
      if dirEntry.isDir:
         nsize = self.vbr.clusterBytes * len(chain)
      else:
         nsize = dirEntry.fileSize

      if nsize == 0:
         # Nothing to read
         return entrySectors, buf

      for cluster in chain:
         assert nsize > 0

         data = self.readCluster(cluster)

         clusterSector = (self.vbr.dataStartSector +
                          cluster * self.vbr.sectorsPerCluster)

         if nsize >= self.vbr.clusterBytes:
            nsectors = self.vbr.sectorsPerCluster
            nsize -= self.vbr.clusterBytes
         else:
            # trim last cluster to the remaining size of the entry
            data = data[:nsize]
            nsectors = int(math.ceil(nsize / self.sectorSize))
            nsize = 0

         for i in range(nsectors):
            entrySectors.append(clusterSector + i)

         if not dirEntry.isDir and outputStream is not None:
            outputStream.write(data)
         elif dirEntry.isDir or doBuffering:
            buf += data

      return entrySectors, buf

   def exists(self, path):
      """Check if the path exists.
      """
      try:
         dirEntry = self.getDirEntry(path)
         return dirEntry is not None
      except FileNotFoundError:
         return False

   def getFileSectors(self, filename, outputStream=None):
      """Retrieve all the sectors of the given file.

      @param outputStream: write to here instead of storing in buffer

      Return a tuple of []filesectors, bytes of file contents
      """
      dirEntry = self.getDirEntry(filename)
      return self.getDirEntrySectors(dirEntry, outputStream)


class Fat16Partition(FatPartition):
   """FAT16 (and VFAT) container class.
   """
   def __init__(self, diskPath, partitionOffset=0, sectorSize=512, logger=None):
      super().__init__(diskPath, 16, partitionOffset, sectorSize, logger)


class Libvfat(SharedLibrary):
   """Python bindings to libvfat.so.
   """
   path = "libvfat.so"

   def __init__(self, name=None, **kwargs):
      if name is None:
         name = self.path
      super().__init__(name, **kwargs)
      self.VFAT_CreateFS.argtypes = [c_int, c_uint64, c_uint32, c_uint32,
                                     c_uint64, c_char_p, c_void_p]
      self.VFAT_CreateFS.restype = c_int


if IS_ESX:
   from vmware import vsi

def mkfsvfatd(volumeFd, blockSize, physBlockSize, numBlocks, fsName, offset=0,
              uuid=None):
   """Format the given volume to VFAT.
   """
   assert numBlocks <= VFAT_MAX_SECTORS

   uuidBuffer = bytearray(SIZEOF_FS_UUID)
   uuid_t = c_ubyte * SIZEOF_FS_UUID

   if uuid is not None:
      # Use the custom UUID if it's valid
      uuidBuffer = bytearray(EsxFsUuid.fromString(uuid))

   with Libvfat() as libvfat:
      libvfat.VFAT_CreateFS(volumeFd, offset, blockSize, physBlockSize,
                            numBlocks, fsName.encode(),
                            uuid_t.from_buffer(uuidBuffer))

   return EsxFsUuid(uuidBuffer)

def mkfsvfat(volPath, blockSize, physBlockSize, numBlocks, fsName,
             isRegularFile=False, offset=0, uuid=None):
   """Format the given block device or regular file to VFAT.

   @param volPath: Path to the volume to be formatted.
   @param blockSize: Size of a block in bytes.
   @param physBlockSize: Physical size of a block in bytes.
   @param numBlocks: Number of blocks for this volume.
   @param fsName: String to be used as volume label.
   @param isRegularFile: Boolean indicating if the volume is backed by a regular
                         file.
   @param offset: An offset in bytes indicating where to start formatting in the
                  given volume.
   @param uuid: UUID to be used for the volume.
   """
   if not IS_SYSTEM_STORAGE_NEXT_ENABLED:
      # Deprecated, must be removed when SystemStorageNext has taken over.
      cmd = ['/bin/vmkfstools', '-C', 'vfat', '-S', fsName, volPath]
      subprocess.check_call(cmd)
      return

   flags = os.O_RDWR if isRegularFile else os.O_RDWR | os.O_DIRECT | os.O_SYNC
   fd = os.open(volPath, flags)
   try:
      return mkfsvfatd(fd, blockSize, physBlockSize, numBlocks, fsName, offset,
                       uuid)
   finally:
      os.close(fd)

if IS_ESX:
   def mount(volumePath):
      """Mount the given volume.

      @param volumePath Example: naa.6b083fe0c0bd59001b807e841043f99b:2
      """
      vsi.set("/vmkModules/vfat/mount", [volumePath])

   def umount(volumePath):
      """Unmount the given volume.

      @param volumePath Example: naa.6b083fe0c0bd59001b807e841043f99b:2
      """
      vsi.set("/vmkModules/vfat/umount", [volumePath])


def _mtools(exe, volume, dest, src=None, byteOffset=0, overwrite=False,
            options=None):
   """Helper function to run an mtools command.

   @src: list of source files
   @overwrite: set to True to use overwrite as name clash resolution.

   Returns the decoded stdout string.
   """
   if byteOffset > 0:
      volume += '@@%u' % byteOffset

   cmd = [exe, '-i', volume]

   if options is not None:
      cmd += options

   if overwrite:
      cmd += ['-Do']

   if src is not None:
      cmd += src

   cmd += [dest]

   if IS_ESX:
      from vmware.runcommand import runcommand, RunCommandError

      cmdStr = 'export MTOOLS_SKIP_CHECK=1 && ' + ' '.join(cmd)
      try:
         rc, out, err = runcommand(cmdStr, redirectErr=False)
         if rc != 0:
            raise RuntimeError("failed to execute mtools command:\n"
                               "COMMAND: %s\nSTDOUT: %s\nSTDERR: %s" %
                               (cmdStr, out.decode(), err.decode()))
         return out.decode()
      except RunCommandError as e:
         raise RuntimeError("failed to execute mtools command: %s" % e)
   else:
      env = os.environ.copy()
      env['MTOOLS_SKIP_CHECK'] = '1'
      try:
         res = subprocess.run(cmd, env=env, check=True,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
         return res.stdout.decode('utf-8')
      except subprocess.CalledProcessError as e:
         raise RuntimeError("failed to execute mtools command:\n"
                            "COMMAND: %s\nSTDOUT: %s\nSTDERR: %s" %
                            (" ".join(cmd), e.stdout.decode(),
                             e.stderr.decode()))

def mcopy(volume, srcFiles, dest=None, byteOffset=0, srcIsPart=False, exe=None):
   """Copy a file to or from a VFAT partition using mcopy.

   @srcIsPart: set to True when the src is in the partition, i.e. when copying
   from the partition; otherwise it means copying to the partition.
   """
   options = None
   for srcFile in srcFiles:
      if os.path.isdir(srcFile):
         options = ['-s']

   if exe is None:
      exe = 'mcopy'
   if dest is None:
      dest = '/'
   if srcIsPart:
      srcFiles = ['::' + s for s in srcFiles]
   else:
      dest = '::' + dest

   # Overwrite with name clashes: overwrite=True
   _mcf = lambda b: _mtools(exe, volume, dest, src=b, byteOffset=byteOffset,
                            overwrite=True, options=options)

   ESXI_ARG_MAX_LIMIT  = 4000 # not an accurate limit(4096)
   if len(' '.join(srcFiles)) < ESXI_ARG_MAX_LIMIT:
      _mcf(srcFiles)
      return

   batch = []
   for f in srcFiles:
      if len(' '.join(batch))  + len(f) < ESXI_ARG_MAX_LIMIT:
         batch.append(f)
      else:
         _mcf(batch)
         batch = [f]
   if batch:
      _mcf(batch)


def mmd(volume, dirPath, byteOffset=0, exe=None):
   """Create a directory in a VFAT partition using mmd.
   """
   if exe is None:
      exe = 'mmd'
   dest = '::' + dirPath
   # Name clash resolution must be supplied, even though mmd always return 1
   # if the folder exists.
   _mtools(exe, volume, dest, byteOffset=byteOffset, overwrite=True)

def mdir(volume, dirPath):
   """List a directory in the given VFAT partition.
   """
   return _mtools('/bin/mdir', volume, dirPath)
