########################################################################
# Copyright (c) 2010-2024 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
########################################################################

"""This module contains classes necessary to parse, extract, and validate
   VIB archives and VIB XML meta-data.
"""

# Python library modules
import datetime
import io
import logging
import operator
import os
import re
import shutil
import stat
import struct
import subprocess
import sys
import tarfile
import tempfile

# esximage modules
from . import (AcceptanceLevels, CHECKSUM_EXEMPT_PAYLOADS, Errors,
   QUICKPATCH_STAGE1_ENABLED, Version)
from .ConfigSchema import ConfigSchema, ConfigSchemaSoftwareTag
from .Utils import ArFile, EsxGzip, PathUtils, VmTar, XmlUtils
from .Utils.HashedStream import HashedStream, HashError
from .Utils.Misc import isString
from .VibExports import getLoaderForVibPath

DEFAULT_ZSTD_COMPRESS_LEVEL = 12
PAYLOAD_READ_CHUNKSIZE = 1024 * 1024

SIGN_PKCS = 'sig.pkcs7'
CONFIG_SCHEMA_DIR = "usr/lib/vmware/configmanager/schemas"
QUICKPATCH_SCRIPT_DIR = "usr/lib/vmware/lifecycle/quickPatch/"

# VibSign module is lazy loaded in AcceptanceLevels.
from .AcceptanceLevels import loadVibSign

# Use the XmlUtils module to find ElementTree.
etree = XmlUtils.FindElementTree()

SCHEMADIR = XmlUtils.GetSchemaDir()

EXTENSION_ORIG_DESC = ".orig"
EXTENSION_ORIG_SIG = ".sig"

logger = logging.getLogger(__name__)

try:
   import zstandard as zstd
   HAS_ZSTANDARD_PYTHON = True
except ImportError:
   HAS_ZSTANDARD_PYTHON = False

class HwPlatform(object):
   """A class for representing a hardware platform.
         Attributes (read-only Properties):
            * vendor - A string representing the hardware vendor.
            * model  - A string representing the hardware model.
   """
   def __init__(self, vendor='', model=''):
      """Class constructor.
            Parameters:
               * vendor - An optional string value to assign to the vendor
                          attribute.
               * model  - An optional string value to assign to the model
                          attribute.
      """
      self._vendor = vendor
      self._model = model

   def __hash__(self):
      # We want to support adding this to a set(), so we must be hashable.
      # Incidentally, this is also why our attributes are made available as
      # read-only properties. -RS
      return hash((self._vendor, self._model))

   def __eq__(self, other):
      mine = (self._vendor, self._model)
      theirs = (other._vendor, other._model)
      return mine == theirs

   def __ne__(self, other):
      return not self.__eq__(other)

   def __str__(self):
      return '-'.join(x for x in (self._vendor, self._model) if x)

   vendor = property(lambda self: self._vendor)
   model  = property(lambda self: self._model)

   @classmethod
   def FromXml(cls, xml):
      """Creates an HwPlatform instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new HwPlatform object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse HwPlatform XML data: %s." % e)

      vendor = xml.get("vendor", "").strip()
      if not vendor:
         raise ValueError("hwplatform must specify vendor.")

      model = xml.get("model", "").strip()
      return cls(vendor, model)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("hwplatform")
      xml.set("vendor", self.vendor)
      if self.model:
         xml.set("model", self.model)
      return xml

   def MatchProblem(self, other):
      '''Determine if this HWPlatform object matches another.
         Parameters:
            * other - Another instance of HWPlatform.
         Return:
            * None  - If self.vendor and self.model match the hardware
                      platform 'other'. Note that if
                      self.vendor is '', it will match any value of
                      other.vendor. If self.model is '', it will match any
                      value of other.model.
            * problem - a tuple (attr, thisval, otherval), if there is a mis-
                        match. If self.vendor != other.vendor, then attr will
                        be "vendor"; if the models don't match then attr will be
                        "model". thisval contains the vendor or model value
                        from this instance of HwPlatform, while otherval
                        contains the vendor or model value from other.
      '''
      if self._vendor and self._vendor != other.vendor:
         return ("vendor", self._vendor, other._vendor)
      if self._model and self._model != other.model:
         return ("model", self._model, other.model)
      return None

class SoftwarePlatform(object):
   """Class representing a software platform.
      Attributes:
         version       - version of the platform requirement, for example
                         7.0 / 7.* / 7.0.*, this is optional for a VIB.
         locale        - locale of the platform, can be empty.
         productLineID - product, typically embeddedEsx / esxio.
   """
   # XXX: this differs from schema: VUM test bulletins can have version "e.x.p",
   # NSX-T has version like "7.*.*", EAM test bulletins can have version "*.*".
   VERSION_REGEX = r'^((\d+\.(\*|\d+)(\.(\*|\d+))?)|(e\.x\.p)|(\*\.\*))$'

   PRODUCT_EMBEDDEDESX = 'embeddedEsx'  # implies esxi-x64
   PRODUCT_ESX = 'esx'
   PRODUCT_ESXIO_ARM = 'esxio'  # implies esxio-arm
   PRODUCT_REGEX = r'^(%s|%s|%s)$' % \
                   (PRODUCT_EMBEDDEDESX, PRODUCT_ESX, PRODUCT_ESXIO_ARM)

   PLATFORM_SET_ESX80 = set([PRODUCT_EMBEDDEDESX, PRODUCT_ESXIO_ARM])
   PLATFORM_SET_LEGACY = set([PRODUCT_EMBEDDEDESX])

   def __init__(self, version, locale, productLineID):
      """Class constructor.
      """
      self.SetVersion(version)
      self.SetLocale(locale)
      self.SetProduct(productLineID)

   def __str__(self):
      return '_'.join(x for x in (self._productLineID, self._version,
                                  self._locale) if x)

   def __eq__(self, other):
      return (self._version == other._version and
              self._locale == other._locale and
              self._productLineID == other._productLineID)

   def __ne__(self, other):
      return (self._version != other._version or
              self._locale != other._locale or
              self._productLineID != other._productLineID)

   def SetVersion(self, version):
      """Verify and set version.
      """
      if not isinstance(version, str) or not version:
         raise ValueError('Platform version must be a non-empty string')
      elif not re.match(self.VERSION_REGEX, version):
         raise ValueError("Invalid platform version '%s'" % version)
      else:
         self._version = version

   def SetLocale(self, locale):
      """Verify and set locale.
      """
      if not isinstance(locale, str):
         raise ValueError('Platform locale must be a string')
      self._locale = locale

   @classmethod
   def _validateProduct(cls, productLineID):
      """Validates a productLineID.
      """
      if not isinstance(productLineID, str) or not productLineID:
         raise ValueError('Platform productLineID must be a non-empty string')
      elif not re.match(cls.PRODUCT_REGEX, productLineID):
         logger.warning("Invalid platform productLineID '%s'", productLineID)

   @classmethod
   def ValidateProducts(cls, productLineIDs):
      """Validates a list of productLineIDs.
      """
      if not productLineIDs:
         return

      invalidVals = set()
      for productLineID in productLineIDs:
         try:
            cls._validateProduct(productLineID)
         except ValueError:
            invalidVals.add(productLineID)
      if invalidVals:
         raise ValueError("Empty or invalid productLineIDs: '%s'"
                          % ','.join(invalidVals))

   def SetProduct(self, productLineID):
      """Verify and set productLineID.
      """
      self._validateProduct(productLineID)
      self._productLineID = productLineID

   version = property(lambda self: self._version, SetVersion)
   locale = property(lambda self: self._locale, SetLocale)
   productLineID = property(lambda self: self._productLineID, SetProduct)

   @classmethod
   def FromXml(cls, xml):
      """Creates an instance from XML. Input should be an instance of
         ElementTree.
      """
      return cls(xml.get('version'), xml.get('locale'),
                 xml.get('productLineID'))

   def ToXml(self, isVmwareXml=False):
      """Serialize the object to XML. If isVmwareXml is set, tag name will
         be swPlatform instead of softwarePlatform.
      """
      tagName = 'swPlatform' if isVmwareXml else 'softwarePlatform'
      elem = etree.Element(tagName)
      elem.set('version', self._version)
      elem.set('locale', self._locale)
      elem.set('productLineID', self._productLineID)
      return elem

class VibRelation(object):
   """Describes a relationship between two VIBs, such as a conflict,
      requirement, or replacement.
   """

   RELATIONS = ("<<", "<=", "=", ">=", ">>")

   RELRE = r'\s?(<[<=]|=|>[>=])\s?'
   relre = re.compile(RELRE)

   def __init__(self, name, relation=None, version=None, implicit=False):
      """Class consctructor.
            Parameters:
               * name     - The name of the relation.
               * relation - An operator describing whether the relation matches
                            only particular versions. If specified, must be one
                            of "<<", "<=", "=", ">=" or ">>". ("<<" and ">>"
                            indicate less than and greater than.) If this
                            parameter is specified, the version parameter must
                            also be specified.
               * version  - An instance of Version.VibVersion used for
                            comparison based on the relation operator. If this
                            parameter is specified, version must also be
                            specified.
      """
      self.name = name
      self.relation = relation
      self.version = version
      self.implicit = implicit
      if relation is not None:
         self.id = "%s %s %s" % (name, relation, str(version))
      else:
         self.id = name

   def __eq__(self, other):
      """Determines if this object describes the same relationship as another
         VibRelation object.
            Parameters:
               * other - An instance of VibRelation.
            Returns: True or False, depending on whether the objects define the
                     same relationship or not.
      """
      return (self.name == other.name and
              self.relation == other.relation and
              self.version == other.version)

   def __ne__(self, other):
      return not self.__eq__(other)

   __str__ = lambda self: str(self.id)

   def matchesprovide(self, provide):
      """Returns True if the provide matches this object."""
      if self.name != provide.name:
         return False
      if not self.relation:
         return True
      if not provide.version:
         return False

      # If our relation only specifies version, then we don't compare release.
      compare = lambda x, y: (x > y) - (x < y)
      if self.version.release:
         rc = compare(provide.version, self.version)
      else:
         rc = compare(provide.version.version, self.version.version)

      if self.relation == "<<":
         return rc == -1
      if self.relation == "<=":
         return rc == -1 or rc == 0
      if self.relation == "=":
         return rc == 0
      if self.relation == ">=":
         return rc == 0 or rc == 1
      if self.relation == ">>":
         return rc == 1

   @classmethod
   def FromString(cls, relstr):
      """Creates a VibRelation instance from a string of the form
         '<name> [(<</<=/=/>=/>>) (version)]'.
         Returns: A new VibRelation object.
         Raises:
            * ValueError - If an illegal operator is in the string, or operator
                           was found but version was not found
      """
      parts = cls.relre.split(relstr)
      partslen = len(parts)
      if partslen == 1:
         return cls(parts[0].strip())
      elif partslen < 3 or not parts[2].strip():
         raise ValueError("Relation found but version not found")
      elif partslen > 3:
         raise ValueError("Illegal operator or more than one operator used")
      return cls(parts[0].strip(), parts[1],
                 Version.VibVersion.fromstring(parts[2].strip()))

   @classmethod
   def FromXml(cls, xml):
      """Creates a VibRelation instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibRelation object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse VIB relation XML data: %s." % e)

      name = xml.get("name")
      if not name:
         raise ValueError("name attribute is missing or empty.")
      relation = xml.get("relation")
      if relation  and relation not in cls.RELATIONS:
         raise ValueError("Invalid relation value: %s." % relation)
      version = xml.get("version")
      if relation and not version:
         raise ValueError("relation attribute exists and is non-empty, but "
                          "version attribute is missing or empty.")
      if version and not relation:
         raise ValueError("version attribute exists and is non-empty, but "
                          "relation attribute is missing or empty.")
      if version:
         try:
            version = Version.VibVersion.fromstring(version)
         except Exception as e:
            raise ValueError("version attribute has invalid value '%s'."
                             % version)

      return cls(name, relation, version)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("constraint")
      xml.set("name", self.name)
      if self.relation and self.version:
         xml.set("relation", self.relation)
         xml.set("version", str(self.version))
      return xml


class VibProvide(object):
   "Describes a feature provided by a VIB."
   def __init__(self, name, version=None, implicit=False):
      """Class consctructor.
            Parameters:
               * name     - The name of the provided feature.
               * version  - An optional instance of Version.VibVersion.
               * implicit - The object represents an implicit provide using the
                            package name and version. (Defaults to False.)
      """
      self.name = name
      self.version = version
      self.implicit = implicit
      if version is not None:
         self.id = "%s = %s" % (name, str(version.versionstring))
      else:
         self.id = name

   def __eq__(self, other):
      """Determines if this object describes the same provide as another
         VibProvide object.
            Parameters:
               * other - An instance of Provide.
            Returns: True or False, depending on whether the objects define the
                     same provide or not.
      """
      return (self.name == other.name and
              self.version == other.version)

   def __ne__(self, other):
      return not self.__eq__(other)

   __str__ = lambda self: self.id

   @classmethod
   def FromString(cls, provstr):
      """Creates a VibProvide instance from a string of the form:
         '<provide-name> [= <provide-version>]'
         Returns: A new VibProvide object.
      """
      if '=' in provstr:
         name, ver = provstr.split('=')
         return cls(name.strip(), Version.VibVersion.fromstring(ver.strip()))
      else:
         return cls(provstr.strip())

   @classmethod
   def FromXml(cls, xml):
      """Creates a VibProvide instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibProvide object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse VIB provide XML data: %s." % e)

      name = xml.get("name")
      if not name:
         raise ValueError("name attribute is missing or empty.")
      version = xml.get("version")
      if version:
         try:
            version = Version.VibVersion.fromstring(version)
         except Exception as e:
            raise ValueError("version attribute has invalid value '%s'."
                             % version)
      return cls(name, version)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("provide")
      xml.set("name", self.name)
      if self.version:
         xml.set("version", str(self.version))
      return xml


class Checksum(object):
   """Represents a checksum.
      Attributes:
         * checksumtype - A string describing the algorithm used to compute
                          the checksum.
         * checksum     - The checksum data itself
         * verifyprocess - The type of decompression or other post-processing
                          that needs to be done on the payload prior to
                          computing the checksum.  "" means the checksum can
                          be computed directly on the payload.
   """
   VERIFY_PROCESSES = ("", "gunzip", "txt-mle")

   def __init__(self, checksumtype="sha-256", checksum='',
                verifyprocess=""):
      self.checksumtype = checksumtype
      self.checksum = checksum
      if verifyprocess not in self.VERIFY_PROCESSES:
         raise AssertionError
      self.verifyprocess = verifyprocess

   def __str__(self):
      return '<checksum type %s: %s>' % (self.checksumtype, self.checksum)

   __repr__ = __str__

   def __eq__(self, other):
      return (self.checksumtype == other.checksumtype and
              self.checksum == other.checksum and
              self.verifyprocess == other.verifyprocess)

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a Checksum instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new Checksum object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse checksum XML data: %s." % e)

      checksumtype = xml.get("checksum-type", "").strip()
      if not checksumtype:
         raise ValueError("checksum-type attribute was not found")

      verifyprocess = xml.get("verify-process", "").strip()
      checksum = xml.text.strip() if xml.text else ""

      return cls(checksumtype, checksum, verifyprocess)

   def ToXml(self):
      """Serializes a Checksum object to XML.
         Returns: a etree.Element object.
      """
      elem = etree.Element("checksum")
      elem.set("checksum-type", self.checksumtype)
      if self.verifyprocess:
         elem.set("verify-process", self.verifyprocess)
      elem.text = self.checksum
      return elem


class BaseVib(object):
   """The BaseVib class manages VIB metadata and does not contain methods
      or properties that work with .vib files or that are specific to any type
      of VIB. It contains properties and methods common to any type of VIB.

      VIB objects are intended to be hashable based on a subset of properties.

      Class Variables:
         * VIB_VERSION        - A constant string for the version of the
                                current VIB Specification
         * TYPE_BOOTBANK      - A constant representing a VIB type containing
                                ESXi bootbank payloads
         * VIB_SCHEMA         - The default path of an XML schema that can be
                                used to verify that VIB XML data is valid.
         * TYPE_LOCKER        - A constant representing a VIB type containing
                                files for product locker.

      Attributes:
         * id                - A string giving the unique ID of the VIB.
                               Read-only.
         * vibtype           - One of TYPE_BOOTBANK or TYPE_LOCKER, specifying
                               the payload type of the VIB.
                               Can only be set by the constructor.
         * name              - A user-friendly string giving the name of the
                               VIB. Not required to be unique.
         * version           - An instance of Version, specifying the version
                               information for the VIB.
         * versionstr        - Read-only string representation of version
         * vendor            - A string representing the vendor or publisher
                               of this VIB
         * summary           - A string giving a user-friendly, one-line
                               summary of the VIB contents.
         * description       - A string giving a user-friendly, detailed
                               description of the software in the VIB.
         * releasedate       - A float value giving the date and time the VIB
                               was built or released (as returned by
                               time.time()). May also be None if unknown.
         * urls              - A dict with each key being a unique, friendly
                               identifier for a KB article or other in depth
                               information about this VIB or the issues it is
                               resolving, and the corresponding value being the
                               URL string to the KB article or information.
         * depends           - A list of VibRelations instances, specifying
                               dependency information for the VIB.
         * conflicts         - A list of VibRelations instances, specifying
                               constraints for VIBs that conflict with this VIB
         * replaces          - A list of VibRelations instances, specifying
                               VIBs that this VIB replaces.
         * provides          - A list of Provides constraint instances, specify-
                               ing virtual packages or capabilities of this VIB
                               that other VIBs may have a dependency on.
         * compatibleWith    - A list of VibRelations instances, specifying
                               entities that this VIB is compatible with, but
                               which this VIB is not dependent on for install.
         * swtags            - A list of software tags that may be used for
                               information or filtering.
         * relativepath      - A path relative to the location of metadata.zip
                               giving the location of the VIB.
         * packedsize        - The number of bytes of the entire VIB archive
         * checksum          - The MD5/SHA checksum of the VIB archive
         * scanned           - A boolean indicating whether this VIB has been
                               scanned against the host or not.
         * installdate       - A float value giving the date and time the VIB
                               was installed (as returned by time.time()). May
                               also be None if VIB was not installed, or the
                               value is otherwise not applicable.
   """
   VIB_VERSION = '2.0.0'

   TYPE_BOOTBANK = 'bootbank'
   TYPE_META = 'meta'
   TYPE_LOCKER = 'locker'
   vibtypes = (TYPE_BOOTBANK, TYPE_META, TYPE_LOCKER)

   _vib_classes = dict()

   VIB_SCHEMA = os.path.join(SCHEMADIR, "vib20-metadata.rng")

   # These ATTRS must be equal between two VIBs that are being merged. If any
   # are unequal, this will result in an exception when calling MergeVib().
   ATTRS_TO_VERIFY = ('vibtype', 'name', 'version', 'vendor')
   # When merging two VIBs, the following attributes will be taken from the VIB
   # with the newer releasedate. However, if two VIBs have the same releasedate
   # but non-matching attributes, this will still cause an exception.
   ATTRS_TO_COPY = ('summary', 'description', 'releasedate', 'urls', 'depends',
                    'conflicts', 'replaces', 'provides', 'compatibleWith',
                    'swtags')

   def __init__(self, **kwargs):
      """Class constructor.
            Parameters:
               * kwargs - A list of keyword arguments used to populate the
                          VIB's attributes.
            Returns: A new VIB object.
            Raises:
               * TypeError  - On any unrecognized keyword argument.
               * ValueError - Illegal value passed for a valid keyword (e.g.,
                              wrong type).
      """
      # NOTE: please update ATTRS_TO_COPY if new attr is being added.
      #       Subclasses need to update ATTRS_TO_COPY if they want their
      #       attributes being copied in MergeVib operation.
      self._vibtype          = kwargs.pop('vibtype', self.TYPE_BOOTBANK)
      self.name              = kwargs.pop('name', '')
      self.version           = kwargs.pop('version', Version.VibVersion(""))
      self.vendor            = kwargs.pop('vendor', '')
      self.summary           = kwargs.pop('summary', '')
      self.description       = kwargs.pop('description', '')
      self.releasedate       = kwargs.pop('releasedate', None)
      self.urls              = kwargs.pop('urls', {})
      self.depends           = kwargs.pop('depends', list())
      self.conflicts         = kwargs.pop('conflicts', list())
      self.replaces          = kwargs.pop('replaces', list())
      self.provides          = kwargs.pop('provides', list())
      self.compatibleWith    = kwargs.pop('compatibleWith', list())
      self.swtags            = kwargs.pop('swtags', list())
      self.relativepath      = kwargs.pop('relativepath', '')
      self.packedsize        = kwargs.pop('packedsize', 0)
      self.checksum          = kwargs.pop('checksum', Checksum())
      self._scanned          = kwargs.pop('scanned', False)
      self.installdate       = kwargs.pop('installdate', None)
      self.remotelocations   = kwargs.pop('remotelocations', list())

      if kwargs:
         badkws = ', '.join("'%s'" % kw for kw in kwargs)
         raise TypeError("Unrecognized keyword argument(s): %s." % badkws)

      # Always add implicit provide for self.name, self.version.
      provide = VibProvide(self.name, self.version, implicit=True)
      if provide not in self.provides:
         self.provides.append(provide)

      # Always add implicit replaces.
      replace = VibRelation(self.name, "<<", self.version, implicit=True)
      if replace not in self.replaces:
         self.replaces.append(replace)

      # _signeddesctext is used to store the following
      # 1) The descriptor that gets signed.
      # 2) The original signed descriptor from the vib database
      self._signeddesctext = ''

      # _pkcs7 is used to store a list of signature of _signeddesctext,
      # each could be one of the following
      # 1) The signature from the vib database
      # 2) The newly generated signature for a vib
      self._pkcs7 = []

   def setvibtype(self, value):
      if value in self.vibtypes:
         self._vibtype = value
      else:
         raise ValueError(value + " is not a valid VIB type")

   vibtype = property(lambda self: self._vibtype, setvibtype)
   id      = property(lambda self: '%s_%s_%s_%s' % (self.vendor, self._vibtype,
                                 self.name, str(self.version)))
   versionstr = property(lambda self: str(self.version))
   __str__ = lambda self: "%s-%s" % (self.name, self.version)

   def GetSignature(self):
      '''Returns a byte string that contains all signatures joined by new lines.
      '''
      return b'\n'.join(self._pkcs7)

   def GetOrigDescriptor(self):
      return self._signeddesctext

   def SetSignature(self, pkcs7):
      '''Take a string that contains all signatures joined by new lines,
         set self._pkcs7 to be a list of the signatures in bytes.
      '''
      if sys.version_info[0] >= 3 and isinstance(pkcs7, str):
         pkcs7 = pkcs7.encode()
      self._pkcs7 = self._getPkcs7List(pkcs7)

   def ClearSignature(self):
      '''Clear the list of signature to start fresh.
      '''
      del self._pkcs7[:]

   def SetOrigDescriptor(self, signeddesctext):
      self._signeddesctext = signeddesctext

   def _getPkcs7List(self, pkcs7):
      '''Convert a byte string containing multiple pkcs7 signatures to a list.
      '''
      PKCS7_HEADER = b'-----BEGIN PKCS7-----'
      indexes = [m.start() for m in re.finditer(PKCS7_HEADER, pkcs7)] + \
                [len(pkcs7)]
      return [pkcs7[indexes[i]:indexes[i + 1]].strip(b' \n')
              for i in range(len(indexes) - 1)]

   def MergeVib(self, other):
      '''Merges attributes between two VIBs.
            Parameters:
               * other - another VIB object to merge with.
            Returns: A new VIB object.
            Exceptions:
               ValueError - If merge is attempted between two VIBs with
                            non-matching values for any attribute in the
                            ATTRS_TO_VERIFY class variable, or if a merge is
                            attempted between two VIBs with the same
                            releasedate but different values for any attribute
                            in the ATTRS_TO_COPY class variable.
               TypeError  - If the two VIBs are not of the same class
      '''
      kwargs = {}

      if self.__class__.__name__ != other.__class__.__name__:
         msg = "Unsupported types to merge: '%s' and '%s'" % (
               self.__class__.__name__, other.__class__.__name__)
         raise TypeError(msg)

      for attr in self.ATTRS_TO_VERIFY:
         mine = getattr(self, attr)
         theirs = getattr(other, attr)
         if mine != theirs:
            msg = "VIBs %s and %s have unequal values of the '%s' attribute: " \
                  "'%s' != '%s'" % (self.id, other.id, attr, str(mine),
                                    str(theirs))
            raise ValueError(msg)

      if other.releasedate > self.releasedate:
         newer = other
         older = self
         kwargs['relativepath'] = newer.relativepath
         kwargs['remotelocations'] = newer.remotelocations
      elif other.releasedate < self.releasedate:
         newer = self
         older = other
         kwargs['relativepath'] = newer.relativepath
         kwargs['remotelocations'] = newer.remotelocations
      else:
         for attr in self.ATTRS_TO_COPY:
            mine, theirs = getattr(self, attr), getattr(other, attr)
            if mine != theirs:
               if attr == 'swplatforms' and (not mine or not theirs):
                  # PR 2783222: software platforms may be missing in the
                  # metadata VIB when the depot is made by older EPK/VC, set
                  # the value here according to the VIB descriptor. However,
                  # they cannot be unequal while both being non-empty.
                  self.swplatforms = mine or theirs
                  other.swplatforms = theirs or mine
                  continue
               raise ValueError("VIBs %s and %s have unequal values of the "
                                "'%s' attribute: '%s' != '%s'" % (self.id,
                                other.id, attr, getattr(self, attr),
                                getattr(other, attr)))
         newer = self
         older = other
         rls = list(set(self.remotelocations + other.remotelocations))
         kwargs['remotelocations'] = rls
         # When merging happens to VIBs in metadata.zip, make sure we keep
         # non-empty relativepath for remotelocation generation.
         kwargs['relativepath'] = newer.relativepath or older.relativepath

      for attr in self.ATTRS_TO_VERIFY + self.ATTRS_TO_COPY:
         kwargs[attr] = getattr(newer, attr)

      # Preserve installdate, even if VIB metadata has changed.
      installdates = [x for x in (newer.installdate, older.installdate)
                      if x is not None]
      if installdates:
         kwargs["installdate"] = max(installdates)

      return self.__class__(**kwargs)

   @classmethod
   def _XmlToKwargs(cls, xml):
      kwargs = dict()

      kwargs["vibtype"] = xml.findtext("type")

      for tag in ("name", "vendor", "summary", "description"):
         # lxml does not honor "default" argument to findtext. :-(
         kwargs[tag] = (xml.findtext(tag) or "").strip()

      if not kwargs["name"]:
         raise Errors.VibFormatError("Unknown", "Missing VIB name.")

      text = (xml.findtext("version") or "").strip()
      if text:
         try:
            kwargs["version"] = Version.VibVersion.fromstring(text)
            vibnv = "%s-%s" % (kwargs["name"], kwargs["version"])
         except Exception as e:
            msg = "VIB '%s' has invalid version '%s'." % (kwargs["name"], text)
            raise Errors.VibFormatError(kwargs["name"], msg)

      text = (xml.findtext("release-date") or "").strip()
      if text:
         try:
            kwargs["releasedate"] = XmlUtils.ParseXsdDateTime(text)
         except Exception as e:
            msg = "VIB '%s' has invalid release-date: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      # Part of the database; not used in the publish schema.
      text = (xml.findtext("installdate") or "").strip()
      if text:
         try:
            kwargs["installdate"] = XmlUtils.ParseXsdDateTime(text)
         except Exception as e:
            msg = "VIB '%s' has invalid installdate: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      urls = {}
      for elem in xml.findall('urls/url'):
         key = elem.get('key')
         if not key:
            msg = "VIB '%s' url '%s' has missing or invalid key" % \
               (vibnv, elem.text)
            raise Errors.VibFormatError(vibnv, msg)
         urls[key] = elem.text.strip()
      kwargs["urls"] = urls

      for tag in ("depends", "conflicts", "replaces", "compatibleWith"):
         kwargs[tag] = list()
         for elem in xml.findall("relationships/" + tag + "/constraint"):
            try:
               kwargs[tag].append(VibRelation.FromXml(elem))
            except Exception as e:
               msg = "VIB '%s' has invalid %s: %s." % (vibnv, tag, e)
               raise Errors.VibFormatError(vibnv, msg)

      kwargs["provides"] = list()
      for elem in xml.findall("relationships/provides/provide"):
         try:
            kwargs["provides"].append(VibProvide.FromXml(elem))
         except Exception as e:
            msg = "VIB '%s' has invalid provide: %s." % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      kwargs["swtags"] = list()
      for elem in xml.findall("software-tags/tag"):
         text = elem.text.strip()
         kwargs["swtags"].append(text)

      kwargs["relativepath"] = (xml.findtext("relative-path") or '').strip()

      text = (xml.findtext("packed-size") or '0').strip()
      try:
         kwargs["packedsize"] = int(text)
      except Exception as e:
         msg = "VIB '%s' has invalid packed-size: %s" % (vibnv, e)
         raise Errors.VibFormatError(vibnv, msg)

      elem = xml.find("checksum")
      if elem is not None:
         try:
            kwargs["checksum"] = Checksum.FromXml(elem)
         except Exception as e:
            msg = "VIB '%s' has invalid checksum: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      return kwargs

   @classmethod
   def FromXml(cls, xml, origdesc=None, signature=None, validate=False,
               schema=VIB_SCHEMA, **kwargs):
      """Creates a BaseVIB instance from an ElementTree XML Node.
            Parameters:
               * xml       - Must be either an instance of ElementTree, or a
                             string of XML-formatted data.
               * origdesc  - The original descriptor data for the VIB
               * signature - The original signature data for the VIB
               * validate  - If True, XML will be validated against a schema. If
                             False, no validation will be done. Defaults to
                             True.
               * schema    - A file path giving the location of a VIB schema.
               * kwargs    - Initialize constructor arguments from keywords.
                             Primarily useful to provide required id or type
                             arguments when XML data is from a template.
            Returns: A new Vib object.
            Raises:
               * VibFormatError     - If the given XML is not valid XML, or
                                      does not contain required elements or
                                      attributes.
               * VibValidationError - If validation is True, and an error
                                      occurred during validation.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            msg = "Could not parse VIB XML data: %s." % e
            raise Errors.VibFormatError("Unknown", msg)

      name = xml.findtext("name") or ''
      if validate:
         res = cls.Validate(xml, schema)
         if not res:
            msg = "VIB (%s) XML data failed schema validation. Errors: %s" \
                  % (name, res.errorstrings)
            raise Errors.VibValidationError(None, res.errorstrings, msg)

      vibtype = xml.findtext("type")
      if not vibtype:
         vibtype = BaseVib.TYPE_BOOTBANK  # same default as constructor
      if vibtype not in cls.vibtypes:
         raise Errors.VibFormatError(name, "Invalid VIB type '%s'" % vibtype)

      vibclass = cls._vib_classes.get(vibtype, BaseVib)
      xmlkwargs = vibclass._XmlToKwargs(xml)

      if origdesc is not None:
         if not etree.iselement(origdesc):
            try:
               origxml = XmlUtils.ParseXMLFromString(origdesc)
            except Exception as e:
               msg = "Could not parse original VIB XML data: %s." % e
               raise Errors.VibFormatError(name, msg)
         else:
            origxml = origdesc
         if validate:
            res = cls.Validate(origxml, schema)
            name = origxml.findtext("name") or ''
            if not res:
               msg = "Original VIB (%s) XML data failed schema validation. " \
                     "Errors: %s" % (name, res.errorstrings)
               raise Errors.VibValidationError(None, res.errorstrings, msg)

         # Use original as the source of args, except install date and checksum
         # that are present only in image database and metadata zip contexts
         # respectively.
         origkwargs = vibclass._XmlToKwargs(origxml)
         origkwargs['installdate'] = xmlkwargs.get('installdate', None)
         origkwargs['checksum'] = xmlkwargs.get('checksum', Checksum())
         kwargs.update(origkwargs)
      else:
         kwargs.update(xmlkwargs)

      retvib = vibclass(**kwargs)
      if signature is not None:
         retvib.SetSignature(signature)
      if origdesc is not None:
         retvib.SetOrigDescriptor(origdesc)
      return retvib

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("vib", {"version":"5.0"})

      elem = etree.SubElement(xml, "type").text = self._vibtype

      for tag in ("name", "version", "vendor", "summary", "description"):
         etree.SubElement(xml, tag).text = str(getattr(self, tag))

      elem = etree.SubElement(xml, "release-date")
      if self.releasedate is not None:
         elem.text = self.releasedate.isoformat()
      else:
         tz = XmlUtils.UtcInfo()
         elem.text = datetime.datetime.now(tz).isoformat()

      elem = etree.SubElement(xml, "urls")
      for key in sorted(self.urls.keys()):
         urlelem = etree.SubElement(elem, "url")
         urlelem.text = self.urls[key]
         urlelem.set("key", key)

      rel = etree.SubElement(xml, "relationships")
      for tag in ("depends", "conflicts", "replaces", "provides",
                  "compatibleWith"):
         elem = etree.SubElement(rel, tag)
         constraints = getattr(self, tag)
         for constraint in constraints:
            if not constraint.implicit:
               elem.append(constraint.ToXml())

      elem = etree.SubElement(xml, "software-tags")
      for tag in self.swtags:
         subelem = etree.SubElement(elem, "tag").text = tag

      if self.relativepath:
         etree.SubElement(xml, "relative-path").text = self.relativepath
      if self.packedsize:
         etree.SubElement(xml, "packed-size").text = str(self.packedsize)
      if self.checksum.checksum:
         xml.append(self.checksum.ToXml())
      if self.installdate is not None:
         elem = etree.SubElement(xml, "installdate")
         elem.text = self.installdate.isoformat()

      return xml

   def ToXmlString(self):
      try:
         return etree.tostring(self.ToXml(), pretty_print=True)
      except Exception:
         return etree.tostring(self.ToXml())

   @classmethod
   def Validate(cls, xml, schema=VIB_SCHEMA):
      """Validates an XML document against the given schema.
            Parameters:
               * xml    - An ElementTree instance, or a string containing XML
                          data.
               * schema - A file path giving the location of a VIB schema.
            Returns:
               A ValidationResult instance, which evaluates to
               True if the document is valid, False otherwise.
               errorstrings and errors properties can also be evaluated.
            Raises:
               * VibValidationError - If the schema cannot be loaded, if the
                                      schema is not valid, or if the
                                      ElementTree implementation does not
                                      support validation.
               * VibFormatError     - If the xml parameter is not valid XML.
      """
      try:
         schema_obj = XmlUtils.GetSchemaObj(schema)
      except Exception as e:
         raise Errors.VibValidationError(None, [], str(e))

      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            msg = "Could not parse VIB XML data: %s." % e
            raise Errors.VibFormatError(msg, None)

      return XmlUtils.ValidateXml(xml, schema_obj)

   def GetRelativePath(self):
      """Returns a relative name for the vib in a depot tree
            Parameters:
               None
            Returns:
               A relative path
      """
      return os.path.join("vib20", self.name, self.id + ".vib")

   def GetReleaseDatetimeObj(self):
      """Returns a datetime object for the releasedate of the vib.
      """
      date, time = self.releasedate.isoformat().split('T')
      dateStr = date + " " + time.split('.')[0]
      dateFormat = "%Y-%m-%d %H:%M:%S"
      return datetime.datetime.strptime(dateStr, dateFormat)

class QuickPatchScript(object):
   """The QuickPatchScript class represents script details for quick patch.
      Attributes:
         * scriptType   - the detailed type of the script, can only be one
                          of SCRIPT_TYPES.
         * timeout      - the timeout value (in seconds) of the script that
                          comes from the script headers.
         * path         - the path/name of the script
      Quick patch scripts fall into 3 types:
         "scan"            - Check compliance.
         "apply-prepare"   - Preparation of publishing the mount revision
                             (i.e. making the mount revision the default).
         "apply-published" - Action performed after publishing the mount
                             revision.
   """
   # Quick patch constants
   QP_SCRIPT_TYPE_SCAN = "scan"
   QP_SCRIPT_TYPE_APPLY_PREPARE = "apply-prepare"
   QP_SCRIPT_TYPE_APPLY_PUBLISHED = "apply-published"

   SCRIPT_TYPES = (QP_SCRIPT_TYPE_SCAN, QP_SCRIPT_TYPE_APPLY_PREPARE,
                   QP_SCRIPT_TYPE_APPLY_PUBLISHED)

   APPLY_SCRIPT_TYPES = (QP_SCRIPT_TYPE_APPLY_PREPARE,
                         QP_SCRIPT_TYPE_APPLY_PUBLISHED)

   SCAN_SCRIPT_TYPES = (QP_SCRIPT_TYPE_SCAN,)

   def __init__(self, scriptType, timeout, path):
       self.scriptType = scriptType
       self.timeout = timeout
       self.path = path

   def __eq__(self, other):
      return (self.scriptType == other.scriptType and
              self.timeout == other.timeout and
              self.path == other.path)

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def _XmlToKwargs(cls, vibName, xml):
      """Get dict args from xml file and do validation.
      """
      kwargs = dict()

      scriptType = xml.get("type")
      if not scriptType:
         raise ValueError("QuickPatchScript type is missing.")
      if not isinstance(scriptType, str):
         raise ValueError("QuickPatchScript type is not a valid string.")
      scriptType = scriptType.strip()
      if scriptType not in cls.SCRIPT_TYPES:
         raise ValueError("QuickPatchScript type of '%s' is not supported."
                          % scriptType)
      kwargs["scriptType"] = scriptType

      timeout = xml.get("timeout")
      if not timeout:
         raise ValueError("QuickPatchScript timeout is missing.")
      try:
         timeout = int(timeout.strip())
      except Exception:
         raise ValueError("Invalid value '%s' for QuickPatchScript "
                          "timeout." % timeout)
      if timeout <= 0:
         raise ValueError("timeout in QuickPatchScript path must be positive.")
      kwargs["timeout"] = timeout

      # QuickPatch script should be in
      # "usr/lib/vmware/"lifecycle/quickPatch/<VIB name>/"
      path = xml.text
      if not path:
         raise ValueError("QuickPatchScript path is missing.")
      path = path.strip()
      if path.find(QUICKPATCH_SCRIPT_DIR):
         raise ValueError("QuickPatchScript path is invalid.")
      try:
         scriptVibName, _ = path[len(QUICKPATCH_SCRIPT_DIR):].split('/')
      except Exception:
         raise ValueError("Invalid value '%s' for QuickPatchScript path."
                          % path)
      if scriptVibName != vibName:
         raise ValueError("vibName '%s' in QuickPatchScript path does not "
                          "match the VIB name '%s'." %
                          (scriptVibName, vibName))
      kwargs["path"] = path

      return kwargs

   @classmethod
   def FromXml(cls, xml, vibName):
      """Creates a QuickPatchScript instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new QuickPatchScript object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse QuickPatchScript XML data: %s."
                             % e)
      kwargs = cls._XmlToKwargs(vibName, xml)
      return cls(**kwargs)

   def ToXml(self):
      """Serializes a QuickPatchScript object to XML.
         Returns: an etree.Element object.
      """
      xml = etree.Element("script")
      if self.scriptType not in self.SCRIPT_TYPES:
         raise ValueError("QuickPatchScript type '%s' is not supported."
                          % self.scriptType)
      xml.set("type", self.scriptType)
      if not isinstance(self.timeout, int) or self.timeout <= 0:
         raise ValueError("QuickPatchScript timeout '%s' is invalid."
                          % self.timeout)
      xml.set("timeout", str(self.timeout))
      if not isinstance(self.path, str):
         raise ValueError("QuickPatchScript path '%s' is not a valid string."
                          % self.path)
      xml.text = self.path
      return xml

class Payload(object):
   """The Payload class encapsulates a VIB payload of different types.

      Class Variables:
         * TYPE_VGZ                - Payload is vtar.gz format
         * TYPE_TGZ                - Payload is tar.gz format
         * TYPE_BOOT               - Payload is a boot kernel not mappable in
                                     VisorFS
         * TYPE_UPGRADE            - Payload is upgrade data for inclusion in
                                     upgrade ISO images.
         * TYPE_BOOT_COM32_BIOS    - Payload is a boot loader COM32 module.
         * TYPE_BOOT_ISO_BIOS      - Payload is an ISO boot loader.
         * TYPE_BOOT_PXE_BIOS      - Payload is a PXE boot loader.
         * TYPE_BOOT_LOADER_EFI    - Payload is an EFI boot loader.
         * TYPE_ELTORITO_IMAGE_EFI - Payload is an EFI FAT12 boot image.
         * TYPE_TEXT               - Payload is a file that's a plain text file.
         * TYPE_INSTALLER_VGZ      - Payload is a vtar.gz format payload that
                                     is only used for installation.
         * TYPE_VZSTD              - Payload is ZSTD compressed and then wrapped
                                     with a compression level 0 gzip layer.

      Attributes:
         * name         - A name to identify the payload within a VIB
         * payloadtype  - Identifies the payload type, one of TYPE_*
         * bootorder    - A positive integer for ordering BOOT payloads
         * vfatname     - String, VFAT filename in 8.3 format
         * size         - An integer giving the payload size, in bytes
         * isquickpatch - A boolean to specify whether current payload should
                          be mounted to access the quick patch script
         * overlayorder - Used to sort payloads with quick patch overlay
         * uncompressedsize - An integer giving the uncompressed size of a
                              payload, in bytes. This facilitates the accounting
                              of memory reservation impact of mounting the lean
                              script tardisk and patch tardisk(s) during the
                              Quick Patch remediation. Note that this attribute/
                              tag is introduced in 8.0.3 and is only compulsory
                              for a Quick Patch VIB, namely lean and content
                              payloads, while optional to a non-qp VIB.
         * compressionoption - A dictionary of additional input parameters ->
                               related values for zstd compression options.
         * fileobj      - File object that points to a local source file
                          for the payload
         * checksums    - A list of Checksum instances for the payload
   """
   TYPE_VGZ = "vgz"
   TYPE_TGZ = "tgz"
   TYPE_BOOT = "boot"
   TYPE_UPGRADE = "upgrade"
   TYPE_BOOT_COM32_BIOS = "boot_com32_bios"
   TYPE_BOOT_ISO_BIOS = "boot_iso_bios"
   TYPE_BOOT_PXE_BIOS = "boot_pxe_bios"
   TYPE_BOOT_LOADER_EFI = "boot_loader_efi"
   TYPE_ELTORITO_IMAGE_EFI = "eltorito_image_efi"
   TYPE_TEXT = "text"
   TYPE_INSTALLER_VGZ = "installer_vgz"
   TYPE_VZSTD = "vzstd"
   # A special value used for ordering payload in front of other vgz payloads
   BOOT_ORDER_VGZ_BASE = 100

   PAYLOAD_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_BOOT, TYPE_UPGRADE,
                    TYPE_BOOT_COM32_BIOS, TYPE_BOOT_ISO_BIOS,
                    TYPE_BOOT_PXE_BIOS, TYPE_BOOT_LOADER_EFI,
                    TYPE_ELTORITO_IMAGE_EFI, TYPE_TEXT, TYPE_INSTALLER_VGZ,
                    TYPE_VZSTD)
   GZIP_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_BOOT, TYPE_INSTALLER_VGZ)
   # TYPE_VZSTD has a gzip layer but with compression level 0, thus should
   # be excluded from both GZIP_TYPES and NON_GZIP_TYPES.
   GZIP0_TYPES = (TYPE_VZSTD,)
   ALL_GZIP_TYPES = GZIP_TYPES + GZIP0_TYPES
   NON_GZIP_TYPES = tuple(set(PAYLOAD_TYPES) - set(ALL_GZIP_TYPES))
   # VisorFS mountable tardisk types.
   TARDISK_TYPES = (TYPE_VGZ, TYPE_TGZ, TYPE_INSTALLER_VGZ, TYPE_VZSTD)

   # A new way to distinguish payload type regarding quick patch attributes.
   PL_TYPE_QP_SCRIPT = "pl-qp-script"
   PL_TYPE_QP_PATCH = "pl-qp-patch"
   PL_TYPE_NORMAL = "pl-normal"

   # zstd related
   ZSTD_COMPRESSION_NOT_APPLICABLE = ("A non-zstd compressed payload cannot "
      "have the zstd specific compression option.")
   ZSTD_COMPRESSION_LEVEL = "level"
   ZSTD_SUPPORTED_COMPRESSION_OPTIONS = (ZSTD_COMPRESSION_LEVEL,)

   def __init__(self, name, payloadtype, bootorder=0, vfatname="",
                size=0, isquickpatch=False, overlayorder=None,
                uncompressedsize=None, compressionoption=None, fileobj=None,
                checksums=None):
      if len(name) > 15:
         raise ValueError("Name '%s' exceeds 15 characters" % name)
      self.name = name
      if payloadtype not in self.PAYLOAD_TYPES:
         logger.warning("Payload type of '%s' is not supported.", payloadtype)
      self.payloadtype = payloadtype
      self.bootorder = bootorder
      self.vfatname = vfatname
      self.size = size
      self.isquickpatch = isquickpatch
      self.overlayorder = overlayorder
      self.uncompressedsize = uncompressedsize
      self.compressionoption = compressionoption
      if payloadtype == self.TYPE_VZSTD and not compressionoption:
         # Write default compressionoption only for a TYPE_VZSTD payload if
         # compressionoption is not given.
         self.compressionoption = {self.ZSTD_COMPRESSION_LEVEL:
                                   DEFAULT_ZSTD_COMPRESS_LEVEL}
      elif payloadtype != self.TYPE_VZSTD:
         # Ignore compressionoption if a payload is not TYPE_VZSTD.
         self.compressionoption = None
      self.fileobj = fileobj
      if checksums:
         self.checksums = checksums
      else:
         self.checksums = list()
      self.localname = ''

      # Get payload category according to quick patch related attributes.
      self.payloadcategory = self.PL_TYPE_QP_SCRIPT if self.isquickpatch else \
         (self.PL_TYPE_QP_PATCH if self.overlayorder else self.PL_TYPE_NORMAL)

   def __eq__(self, other):
      # size and checksums (particularly the latter) are important for security.
      # Payloads with unequal size or checksums MUST NOT compare equally. This
      # is used in the BaseVib.MergeVib() method.
      mine = (self.name, self.payloadtype, self.bootorder, self.size,
              self.isquickpatch, self.overlayorder, self.compressionoption,
              sorted(self.checksums, key=operator.attrgetter('checksum')))
      theirs = (other.name, other.payloadtype, other.bootorder, other.size,
                other.isquickpatch, other.overlayorder, other.compressionoption,
                sorted(other.checksums, key=operator.attrgetter('checksum')))

      # Uncompressed size is optional and can be missing when a newer VIB
      # was installed on an older ESXi (e.g. HA). However, the value must match
      # if both sizes are not None.
      uncompressedSizeEq = self.uncompressedsize is None or \
         other.uncompressedsize is None or \
         self.uncompressedsize == other.uncompressedsize

      return mine == theirs and uncompressedSizeEq

   def __ne__(self, other):
      return not self.__eq__(other)

   def __repr__(self):
      return "%s: %.3f KB" % (self.name, self.size / 1024.0)

   def GetPreferredChecksum(self, verifyprocess=''):
      """Returns the most secure checksum available for checking the payload
         for a given verify process.
         Currently, it will return the SHA-256 checksum if available.  If not,
         then it returns the SHA-1 checksum.  If neither of the above is
         available, returns the first checksum with the given verifyprocess.
            Parameters:
               * verifyprocess - only return a checksum of particular verify
                                 process, default is empty string, meaning
                                 no special process.
            Returns: A Checksum instance, or None if there is no checksum
                     matching the criteria.
      """
      algomap = dict()
      for checksum in self.checksums:
         if checksum.verifyprocess == verifyprocess:
            algomap[checksum.checksumtype] = checksum
      if "sha-256" in algomap:
         return algomap["sha-256"]
      elif "sha-1" in algomap:
         return algomap["sha-1"]
      else:
         checksums = list(algomap.values())
         return checksums[0] if checksums else None

   def isQuickPatchRelevant(self, scriptsOnly=False):
      """Returns True if the payload is relevant to quick patch transaction.
         For quick patch dry-run(scan) scriptsOnly is True.
      """
      return self.isquickpatch if scriptsOnly else \
               self.isquickpatch or self.overlayorder

   def Copy(self):
      """Creates a Payload instance with attributes copied from this one.

      WARNING (PR 3249540): This logic must create payloads with the same
      attributes as during packaging, so that regenerated VIBs can be
      bit-for-bit identical for hashing. See `esximage.__main__`.
      """
      return self.__class__(**{
         attr: getattr(self, attr)
         for attr in (
            'name',
            'payloadtype',
            'bootorder',
            'vfatname',
            'overlayorder',
            'isquickpatch',
            'uncompressedsize',
         )
      })

   @classmethod
   def _validateCompressionOptions(cls, optionName, optionValue):
      """Checks whether a vzstd payload has valid compression options.
            - Compression level should be an integer within [-1, 19].
         Parameters:
            * optionName - the name of the compression option
            * optionValue - the value of the compression option
         Raises an exception if the validation fails.
      """
      if optionName not in cls.ZSTD_SUPPORTED_COMPRESSION_OPTIONS:
         raise ValueError("The compression option '%s' is not supported." %
                          optionName)

      if optionName == cls.ZSTD_COMPRESSION_LEVEL:
         try:
            level = int(optionValue)
         except ValueError:
            raise ValueError("The zstd compression level '%s' is not an "
                             "integer." % optionValue)
         if level > 19 or level < -1:
            raise ValueError("The zstd compression level cannot be more than "
                             "19, or less than -1, got '%s'." % optionValue)

   @classmethod
   def FromXml(cls, xml):
      """Creates a Payload instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new Payload object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse payload XML data: %s." % e)

      kwargs = dict()

      text = xml.get("name")
      kwargs["name"] = text.strip()
      length = len(kwargs["name"])
      if length < 1 or length > 15:
         raise ValueError("Invalid value '%s' for name." % text)

      text = xml.get("type")
      kwargs["payloadtype"] = text.strip()
      if kwargs["payloadtype"] not in cls.PAYLOAD_TYPES:
         logger.warning("Unrecognized value '%s' for payload type.", text)

      text = xml.get("boot-order", "0")
      try:
         kwargs["bootorder"] = int(text)
      except Exception:
         raise ValueError("Invalid value '%s' for boot-order." % text)
      if kwargs["bootorder"] < 0:
         raise ValueError("Payload boot-order must be non-negative.")

      kwargs["vfatname"] = xml.get("vfat-name", "").strip()

      text = xml.get("size", "0")
      try:
         kwargs["size"] = int(text)
      except:
         raise ValueError("Invalid value '%s' for size." % text)
      if kwargs["size"] < 0:
         raise ValueError("Payload size must be non-negative.")

      text = xml.get("quick-patch")
      if text:
         try:
            kwargs["isquickpatch"] = XmlUtils.ParseXsdBoolean(text.strip())
         except Exception:
            raise ValueError("Invalid value '%s' for quick-patch in "
                             "payload." % text)

      text = xml.get("overlay-order")
      if text:
         try:
            kwargs["overlayorder"] = int(text.strip())
         except:
            raise ValueError("Invalid value '%s' for overlay-order." % text)
         if kwargs["overlayorder"] <= 0:
            raise ValueError("Payload overlay-order value must be positive.")

      text = xml.get("uncompressed-size")
      if text:
         try:
            kwargs["uncompressedsize"] = int(text.strip())
         except:
            raise ValueError("Invalid value '%s' for uncompressed-size." % text)
         if kwargs["uncompressedsize"] < 0:
            raise ValueError("Payload uncompressed-size value must be "
                             "non-negative.")

      # Mandate uncompressedsize attribute of a lean/patch payload required by
      # a Quick Patch VIB.
      if (kwargs.get("isquickpatch") or kwargs.get("overlayorder")) and \
            text is None:
         raise ValueError("Uncompressed size is required for payload '%s' in a "
                          "Quick Patch VIB." % kwargs["name"])

      # In the XML tag, the field compression-option is an optional
      # expandable parameter string to capture any additional input for
      # zstd compression/decompression, whose format will be
      # "optionName=value" or "optionName=value;optionName=value" (i.e.,
      # multiple options separated by ';').
      text = xml.get("compression-option")
      if text is not None:
         if not text:
            raise ValueError("The compression option exists but is empty.")
         if kwargs["payloadtype"] != cls.TYPE_VZSTD:
            raise ValueError(cls.ZSTD_COMPRESSION_NOT_APPLICABLE)
         compOptDict = {}
         try:
            compOpts = text.strip().split(";")
            for compOpt in compOpts:
               compOpt = compOpt.strip()
               if not compOpt:
                  raise ValueError("A compression option before or after "
                                   "';' is empty.")
               k, v = compOpt.split("=")
               k, v = k.strip(), v.strip()
               if not k or not v:
                  raise ValueError("A compression option has an empty name "
                                   "or value.")
               # Check if current compression option is valid.
               cls._validateCompressionOptions(k, v)
               compOptDict[k] = v
            if len(compOptDict):
               kwargs["compressionoption"] = compOptDict
         except Exception as e:
            raise ValueError("Invalid value '%s' for compression option. "
                             "Error: %s" % (text, str(e)))

      elems = xml.findall("checksum")
      checksums = list()
      for elem in elems:
         checksums.append(Checksum.FromXml(elem))
      kwargs["checksums"] = checksums

      return cls(**kwargs)

   def _hasValidCompressionOption(self):
      """Checks whether a vzstd payload has valid compression-option.
         Returns True if all the keywords of compression-option (i.e., keys of
         the non-empty dictionary self.compressionoption) are supported.
         Otherwise, a ValueError would be raised.
         Returns False if self.compressionoption is not a dict instance, or is
         an empty dictionary.
      """
      if isinstance(self.compressionoption, dict) and \
            len(self.compressionoption):
         for k, v in self.compressionoption.items():
            # Check if current compression option is valid.
            self._validateCompressionOptions(k, v)
         return True
      return False

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("payload")
      xml.set("name", self.name)
      xml.set("type", self.payloadtype)
      if self.bootorder > 0:
         xml.set("boot-order", str(self.bootorder))
      if self.vfatname:
         xml.set("vfat-name", self.vfatname)

      # size should be a non-negative integer.
      if not isinstance(self.size, int) or self.size < 0:
         raise ValueError("Payload '%s' has non-integer or negative "
            "'size' value: '%s'." % (self.name, self.size))
      xml.set("size", str(self.size))

      # omit adding "quick-patch" field if self.isquickpatch
      # is False by default
      if self.isquickpatch:
         xml.set("quick-patch", "true")

      if self.overlayorder is not None:
         # overlayorder should be a positive integer if it exists.
         if not isinstance(self.overlayorder, int) or self.overlayorder <= 0:
            raise ValueError("Payload '%s' has non-integer or non-positive "
               "'overlay-order' value: '%s'." % (self.name, self.overlayorder))
         xml.set("overlay-order", str(self.overlayorder))

      if self.uncompressedsize is not None:
         # uncompressedsize should be a non-negative integer if it exists.
         if not isinstance(self.uncompressedsize, int) or \
               self.uncompressedsize < 0:
            raise ValueError("Payload '%s' has non-integer or negative "
               "'uncompressed-size' value: '%s'." % (self.name,
               self.uncompressedsize))
         xml.set("uncompressed-size", str(self.uncompressedsize))

      if self.compressionoption is not None:
         if self.payloadtype != self.TYPE_VZSTD:
            raise ValueError(self.ZSTD_COMPRESSION_NOT_APPLICABLE)
         if self._hasValidCompressionOption():
            compOpts = ';'.join(['%s=%s' % (str(k), str(v)) for k, v in
                                self.compressionoption.items()])
            xml.set("compression-option", compOpts)

      if self.checksums:
         for checksum in self.checksums:
            xml.append(checksum.ToXml())
      return xml


class MaintenanceMode(object):
   """This class represents maintenance mode attributes for a VIB.
         Attributes:
            * remove  - If True, maintenance mode must be enabled when updating
                        or removing the VIB.
            * install - If True, maintenance mode must be enabled on VIB
                        installation.
         Note: Upgrade is a case of removing an old version of a VIB and
               installing a new version. In such cases, the "remove"
               maintenance mode attribute of the VIB being removed should be
               evaluated, and the "install" maintenace mode attribute of the
               new VIB should be evaluated.
   """
   def __init__(self, remove=True, install=None):
      self.remove = remove
      if install is None:
         self.install = remove
      else:
         self.install = install

   def __str__(self):
      return "remove/update: %s, installation: %s" % (self.remove, self.install)

   def __eq__(self, other):
      return self.remove == other.remove and self.install == other.install

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a MaintenanceMode instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new VibProvide object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse maintenance mode XML data: %s." %
                             e)

      install = None
      remove = True
      if xml.text:
         try:
            remove = XmlUtils.ParseXsdBoolean(xml.text)
         except Exception as e:
            raise ValueError(str(e))

      text = xml.get("on-install")
      if text:
         try:
            install = XmlUtils.ParseXsdBoolean(text)
         except Exception as e:
            raise ValueError(str(e))

      text = xml.get("on-remove")
      if text:
         try:
            remove = XmlUtils.ParseXsdBoolean(text)
         except Exception as e:
            raise ValueError(str(e))

      return cls(remove, install)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("maintenance-mode")
      if self.install == self.remove:
         xml.text = self.remove and "true" or "false"
      else:
         xml.set("on-install", self.install and "true" or "false")
         xml.set("on-remove", self.remove and "true" or "false")
      return xml

#
# NOTE: UW and Mem Resource Constraints have been descoped from MN.
# However this class is still kept for OP implementation.
#
class ResourceConstraints(object):
   """A class representing resource pool constraints for userworld executables
      in a VIB.
      Attributes:
         * schedgrouppath    - A string, representing the scheduler group path
                               to which the memory & CPU constraints should be
                               applied.
      For more details on the attributes, please see section 2.1.20 of the VIB
      2.0 functional specification, "Memory and Resource Usage".
      #bora/apps/esxupdate/doc/vib20-spec.tex
      http://vmweb.vmware.com/~evan/MNDesign/vib20-spec.pdf
   """
   XML_TAGS = ("sched-group-path",
               "mem-size-limit-mb",
               )
   XML_TEXT_TAGS = ("sched-group-path",)

   def __init__(self, **kwargs):
      self.memsizelimitmb = kwargs.pop("memsizelimitmb", 70)
      self.schedgrouppath = kwargs.pop("schedgrouppath", "")

      if kwargs:
         badkws = ', '.join("'%s'" % kw for kw in kwargs)
         raise TypeError("Unrecognized keyword argument(s): %s." % badkws)

   def __eq__(self, other):
      for attr in (attr.replace("-", "") for attr in self.XML_TAGS):
         if getattr(self, attr) != getattr(other, attr):
            return False
      return True

   def __ne__(self, other):
      return not self.__eq__(other)

   @classmethod
   def FromXml(cls, xml):
      """Creates a ResourceConstraints instance from an ElementTree XML Node.
            Parameters:
               * xml      - Must be either an instance of ElementTree, or a
                            string of XML-formatted data.
            Returns: A new ResourceConstraints object.
            Raises:
               * ValueError - If the given XML is not valid XML, or does not
                              contain required elements or attributes.
      """
      if not etree.iselement(xml):
         try:
            xml = XmlUtils.ParseXMLFromString(xml)
         except Exception as e:
            raise ValueError("Could not parse resource constraint XML data: "
                             "%s." % e)

      kwargs = dict()
      for tag in cls.XML_TAGS:
         text = xml.findtext(tag)
         if text is not None:
            kw = tag.replace("-", "")
            try:
               if tag in cls.XML_TEXT_TAGS:
                  kwargs[kw] = text.strip()
               else:
                  kwargs[kw] = int(text)
            except Exception:
               raise ValueError("Invalid value '%s' for %s." % (text, tag))

      return cls(**kwargs)

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = etree.Element("resources")
      for tag in self.XML_TAGS:
         value = getattr(self, tag.replace("-", ""))
         etree.SubElement(xml, tag).text = str(value)

      return xml


class ArFileVib(BaseVib):
   """The ArFileVib class manages VIBs based on AR archives, and
      contains properties and methods for handling components of .vib
      files.  It may be used to cosntruct VIB archives -- see the
      WriteVibFile() method for more details.

      Class Variables:

      Attributes:
         * maintenancemode   - A boolean indicating whether the host must be in
                               maintenance mode before installing the VIB.
         * hwplatforms       - A set of HWPlatform instances, indicating the
                               hardware platforms for which the VIB is intended.
         * acceptancelevel   - A number representing the trust or acceptance
                               level of a VIB.  Not settable.
         * filelist          - A list containing the full paths of all files
                               in the (v)tar payload.
         * liveinstallok     - A boolean indicating whether live installation
                               is supported.
         * liveremoveok      - A boolean indicating whether live removal is
                               supported.
         * cimomrestart      - A boolean indicating whether cimom should be
                               restarted after a live install/removal.
         * statelessready    - A boolean indicating whether this VIB is ready
                               for stateless deployment.
         * payloadupdateonly - A boolean indicating whether quick patch is
                               possible with the VIB regarding a special case
                               where no script is required for the VIB.
         * respooldef        - A string indicating the path of the yaml file
                               which defines resource pool used by all the
                               Quick Patch scripts.
         * secpolicydir      - A string indicating the path of the directory
                               that contains security policies for Quick Patch
                               scripts.
         * esxscripts        - A list of QuickPatchScript instances, listing
                               all the scripts that would be executed for quick
                               patch to scan or apply.
         * overlay           - A boolean indicating whether this VIB is allowed
                               to overlay files from a non-overlay VIB
         * payloads          - A list of Payload instances, listing all the
                               payloads contained in this VIB.
         * resources         - An instance of ResourceConstraints.
         * signed            - Boolean property, True if VIB is signed
   """
   ACCEPTANCE_COMMUNITY = 'community'
   ACCEPTANCE_PARTNER = 'partner'
   ACCEPTANCE_ACCEPTED = 'accepted'
   ACCEPTANCE_CERTIFIED = 'certified'
   ACCEPTANCE_LEVELS = (ACCEPTANCE_COMMUNITY, ACCEPTANCE_PARTNER,
                        ACCEPTANCE_ACCEPTED, ACCEPTANCE_CERTIFIED,
                        # Temporary additions for compatibility:
                        'signed', 'unsigned')

   # Map acceptance level values to the value used in the config store
   # acceptance_level schema.
   TO_CS_ACCEPTANCE = {
      ACCEPTANCE_CERTIFIED: "VMWARE_CERTIFIED",
      ACCEPTANCE_ACCEPTED: "VMWARE_ACCEPTED",
      ACCEPTANCE_PARTNER: "PARTNER_SUPPORTED",
      ACCEPTANCE_COMMUNITY: "COMMUNITY_SUPPORTED"
   }
   FROM_CS_ACCEPTANCE = {v:k for k, v in TO_CS_ACCEPTANCE.items()}

   ATTRS_TO_VERIFY = BaseVib.ATTRS_TO_VERIFY + ('acceptancelevel', 'filelist',
         'overlay', 'payloads', 'resources')
   ATTRS_TO_COPY = BaseVib.ATTRS_TO_COPY +  ('maintenancemode', 'hwplatforms',
         'swplatforms', 'liveinstallok', 'liveremoveok', 'payloadupdateonly',
         'respooldef', 'esxscripts', 'secpolicydir')

   def __init__(self, **kwargs):
      self.maintenancemode = kwargs.pop('maintenancemode', MaintenanceMode())
      self.hwplatforms     = kwargs.pop('hwplatforms', set())
      self.acceptancelevel = kwargs.pop('acceptancelevel', "community")
      self.filelist        = kwargs.pop('filelist', set())
      self.liveinstallok   = kwargs.pop('liveinstallok', False)
      self.liveremoveok    = kwargs.pop('liveremoveok', False)
      self.cimomrestart    = kwargs.pop('cimomrestart', False)
      self.statelessready  = kwargs.pop('statelessready', False)
      self.payloadupdateonly = kwargs.pop('payloadupdateonly', None)
      self.respooldef      = kwargs.pop('respooldef', None)
      self.secpolicydir    = kwargs.pop('secpolicydir', None)
      self.esxscripts      = kwargs.pop('esxscripts', list())
      self.overlay         = kwargs.pop('overlay', False)
      self.payloads        = kwargs.pop('payloads', list())
      self.resources       = kwargs.pop('resources', ResourceConstraints())

      self.swplatforms = list()
      for p in kwargs.pop('swplatforms', list()):
         if isinstance(p, SoftwarePlatform):
            self.swplatforms.append(p)
         else:
            # Devkit scons build passes in tuples.
            self.swplatforms.append(SoftwarePlatform(*p))

      BaseVib.__init__(self, **kwargs)

      self._fileorigin = None
      self._arfile = None

      # Flag indicating if the VIB is partial (True) or regular (False)
      self._isPartialVib = False
      # Flag indicating if the Quick Patch partial VIB has all patch payload
      # files downloaded.
      self._hasAllQpPatchPayloadFiles = False

   def __eq__(self, other):
      if type(other) != type(self):
         return False

      for attr in self.ATTRS_TO_VERIFY:
         if getattr(self, attr) != getattr(other, attr):
            return False

      return True

   def __ne__(self, other):
      return not self.__eq__(other)

   signed = property(lambda self: bool(self._pkcs7))

   @property
   def hasConfigSchema(self):
      """Returns if the VIB has a config schema.

         Use hasPublicConfigSchema instead, if the usecase
         is to know whether schema must be exported.
      """
      return self.GetConfigSchemaTag(filterEsxioSchemas=False) is not None


   @property
   def hasPublicConfigSchema(self):
      """Returns if the VIB has a config schema that
         can be exported in depot so that it is consumed
         by config management platform.
      """
      return self.GetConfigSchemaTag() is not None

   @property
   def hasSystemSoftwarePlatform(self):
      """Returns if this VIB supports this system's software platform, this
         attribute is valid only when called on ESXi/ESXio.
      """
      return self.HasPlatform(GetHostSoftwarePlatform())

   @property
   def isQuickPatchVib(self):
      """Returns if this VIB has quick patch lean payload.
      """
      return True if len(self.esxscripts) else False

   def MergeVib(self, other):
      '''Merges attributes between two VIBs.
         BaseVib.MergeVib() will do most of the work, only statelessready
         attribute specific to ArFileVib needs to be merged.
      '''
      retvib = BaseVib.MergeVib(self, other)
      # Set statelessready tag to the 'or' operation of the two
      # input vibs. See PR 1290763.
      attr = 'statelessready'
      retvib.statelessready = getattr(self, attr) or getattr(other, attr)

      # Uncompressed sizes of payloads can become available during an ESXi
      # upgrade (e.g. HA VIB previously installed), they need to be merged.
      # retvib can be a copy of either self or other depends on releasedate,
      # thus both of them may have the info.
      retPayloads = {p.name : p for p in retvib.payloads}
      selfPayloads = {p.name : p for p in self.payloads}
      otherPayloads = {p.name : p for p in other.payloads}
      for name, payload in retPayloads.items():
         if payload.uncompressedsize is None:
            payload.uncompressedsize = selfPayloads[name].uncompressedsize \
               if selfPayloads[name].uncompressedsize is not None else \
               otherPayloads[name].uncompressedsize
      return retvib

   @classmethod
   def _verifyIsVMwareCertified(cls, acceptancelevel, vendor, vibstr, attr):
      """To make sure that only certified VIB from VMware/VMW can have the
         new VIB metadata, e.g., QuickPatch related info.
         If the check fails, raise VibFormatError.
            Parameters:
               * acceptancelevel - acceptance level of the VIB.
               * vendor          - vendor of the VIB.
               * vibstr          - identifying string of the VIB, can be the ID
                                   or a name-version string.
               * attr            - VIB attribute that requires the check.
      """
      if acceptancelevel != cls.ACCEPTANCE_CERTIFIED or \
            vendor.lower() not in ("vmware", "vmw"):
         msg = ("VIB '%s' with acceptance level '%s' and vendor '%s' cannot "
                "have attribute '%s'." % (vibstr, acceptancelevel, vendor,
                attr))
         raise Errors.VibFormatError(vibstr, msg)

   @classmethod
   def _checkHasOverlayOrderPayload(cls, payloads, vibStr):
      """Checks the existence of a payload with overlay-order. This method is
         called when quick-patch payload-update-only is set to true, which
         mandates the presence of at least one overlay-order payload to be
         mounted during a Quick Patch remediation.
      """
      for payload in payloads:
         if payload.payloadcategory == payload.PL_TYPE_QP_PATCH:
            break
      else:
         msg = ("VIB '%s' with Quick Patch 'payload-update-only' set to true "
                "must have a payload with the 'overlay-order' attribute."
                % vibStr)
         raise Errors.VibFormatError(vibStr, msg)

   @classmethod
   def _XmlToKwargs(cls, xml):
      '''Converts XML to kwargs for the constructor.
         With signed ArFile VIB, caller of this method (FromXml) should be
         using the original descriptor for all attributes except installdate.
         This way tampering of the descriptor will not take effect. Tampering
         of the original descriptor can be revealed with signature check.
      '''
      kwargs = BaseVib._XmlToKwargs(xml)

      # Although, if we've gotten this far without a name and version,
      # something is probably wrong.
      vibnv = "%s-%s" % (kwargs.get("name", "(unknown)"),
                         kwargs.get("version", "(unknown)"))

      elem = xml.find("system-requires/maintenance-mode")
      if elem is not None:
         try:
            kwargs["maintenancemode"] = MaintenanceMode.FromXml(elem)
         except Exception as e:
            msg = "VIB '%s' has invalid maintenance mode: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      hwplatforms = set()
      for elem in xml.findall("system-requires/hwplatform"):
         try:
            hwplatforms.add(HwPlatform.FromXml(elem))
         except Exception as e:
            msg = "VIB '%s' has invalid hwplatform: %s" %(vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)
      if len(hwplatforms):
         kwargs["hwplatforms"] = hwplatforms

      swPlatforms = list()
      for elem in xml.findall('system-requires/softwarePlatform'):
         try:
            swPlatforms.append(SoftwarePlatform.FromXml(elem))
         except Exception as e:
            msg = ("VIB '%s' has invalid softwarePlatform: %s"
                   % (vibnv, e))
            raise Errors.VibFormatError(vibnv, msg)
      if swPlatforms:
         kwargs['swplatforms'] = swPlatforms

      filelist = set()
      for elem in xml.findall("file-list/file"):
         if elem.text:
            text = elem.text.strip()
            if text:
               filelist.add(text)
      if len(filelist):
         kwargs["filelist"] = filelist

      text = (xml.findtext("acceptance-level") or "community").strip()
      if text not in cls.ACCEPTANCE_LEVELS:
         msg = "VIB '%s' has invalid acceptance level '%s'." % (vibnv, text)
         raise Errors.VibFormatError(vibnv, msg)
      kwargs["acceptancelevel"] = text

      for tag in ("live-install-allowed", "live-remove-allowed",
                  "cimom-restart", "stateless-ready"):
         text = xml.findtext(tag) or "false"
         try:
            kw = tag.replace("-", "").replace("allowed", "ok")
            kwargs[kw] = XmlUtils.ParseXsdBoolean(text)
         except Exception:
            msg = ("VIB '%s' has invalid %s value '%s'." %
                   (vibnv, tag, text))
            raise Errors.VibFormatError(vibnv, msg)

      vibName = kwargs.get("name", "(unknown)")
      quickPatch = xml.find("quick-patch")
      # If tag <quick-patch> exists, only 2 of the following 3 cases are
      # accepted.
      if quickPatch is not None:
         cls._verifyIsVMwareCertified(kwargs["acceptancelevel"],
            kwargs["vendor"], vibnv, "quick-patch")
         payloadupdateonly = quickPatch.get("payload-update-only")
         # <quick-patch> tag contains the payload-update-only attribute.
         if payloadupdateonly is not None:
            payloadupdateonly = payloadupdateonly.strip()
            try:
               kwargs["payloadupdateonly"] = XmlUtils.ParseXsdBoolean(
                                             payloadupdateonly)
            except Exception:
               msg = ("QuickPatch VIB '%s' has invalid 'payload-update-only' "
                      "attribute '%s'." % (vibnv, payloadupdateonly))
               raise Errors.VibFormatError(vibnv, msg)
            # 1) special case <quick-patch payload-update-only="true"/>.
            # There cannot be <esx-scripts>, <resource-pool-definition> or
            # <security-policy-dir>.
            if kwargs["payloadupdateonly"]:
               if quickPatch.findtext("resource-pool-definition") is not None \
                  or quickPatch.findtext("security-policy-dir") is not None \
                  or quickPatch.find("esx-scripts/script") is not None:
                  msg = ("QuickPatch VIB '%s' has 'payload-update-only=true' "
                         "but still contains one of 'esx-scripts',"
                         "'resource-pool-definition' or 'security-policy-dir'."
                         % vibnv)
                  raise Errors.VibFormatError(vibnv, msg)
            # 2) <quick-patch payload-update-only="false"> is not allowed
            # regardless of with or without scripts.
            else:
               msg = ("QuickPatch VIB '%s' has invalid 'payload-update-only' "
                      "attribute, it must be either true or absent." % vibnv)
               raise Errors.VibFormatError(vibnv, msg)
         # 3) <quick-patch> without the payload-update-only attribute, with
         # sub-tags <resource-pool-definition>, <security-policy-dir>, and
         # <esx-scripts>.
         else:
            respooldef = quickPatch.findtext("resource-pool-definition")
            if not respooldef:
               msg = ("QuickPatch VIB '%s' is missing "
                      "'resource-pool-definition'." % vibnv)
               raise Errors.VibFormatError(vibnv, msg)
            respooldef = respooldef.strip()
            regex = "^" + QUICKPATCH_SCRIPT_DIR + vibName + "/[0-9a-zA-Z-_]+" \
                    + ".yml$"
            if not re.match(regex, respooldef):
               msg = ("QuickPatch VIB '%s' has invalid 'resource-pool-"
                      "definition' location '%s'." % (vibnv, respooldef))
               raise Errors.VibFormatError(vibnv, msg)
            # respooldef is guaranteed to be non-empty now
            kwargs["respooldef"] = respooldef

            secPolDir = quickPatch.findtext("security-policy-dir")
            if not secPolDir:
               msg = ("QuickPatch VIB '%s' is missing 'security-policy-dir'."
                      % vibnv)
               raise Errors.VibFormatError(vibnv, msg)
            secPolDir = secPolDir.strip()
            if not secPolDir.startswith(QUICKPATCH_SCRIPT_DIR + vibName):
               # The directory must be in the Quick Patch script directory.
               msg = ("QuickPatch VIB '%s' has invalid 'security-policy-dir' "
                      "value '%s'." % (vibnv, secPolDir))
               raise Errors.VibFormatError(vibnv, msg)
            kwargs["secpolicydir"] = secPolDir

            esxscripts = list()
            for elem in quickPatch.findall("esx-scripts/script"):
               try:
                  esxscripts.append(QuickPatchScript.FromXml(elem, vibName))
               except Exception as e:
                  msg = ("QuickPatch VIB '%s' has invalid 'esx-scripts': %s"
                         % (vibnv, e))
                  raise Errors.VibFormatError(vibnv, msg)
            kwargs["esxscripts"] = esxscripts
            # Check for esx-scripts can only be included in ToXml() as the
            # input descriptor in vibauthor does not include it.

      # Enable this in OP
      #elem = xml.find("resources")
      #if elem is not None:
      #   try:
      #      kwargs["resources"] = ResourceConstraints.FromXml(elem)
      #   except Exception as e:
      #      msg = "VIB '%s' has invalid resource constraints: %s" % (vibnv, e)
      #      raise Errors.VibFormatError(None, msg)

      text = xml.findtext("overlay")
      if text:
         try:
            kwargs["overlay"] = XmlUtils.ParseXsdBoolean(text)
         except:
            msg = ("VIB '%s' has invalid overlay value '%s'." % (vibnv, text))
            raise Errors.VibFormatError(vibnv, msg)

      kwargs["payloads"] = list()
      payloadnames = set()
      for elem in xml.findall("payloads/payload"):
         try:
            payload = Payload.FromXml(elem)
            if payload.name in payloadnames:
               msg = "Duplicate payload name '%s'." % payload.name
               raise Errors.VibFormatError(vibnv, msg)
            kwargs["payloads"].append(payload)
         except Exception as e:
            msg = "VIB '%s' has invalid payload: %s" % (vibnv, e)
            raise Errors.VibFormatError(vibnv, msg)

      if kwargs.get('payloadupdateonly', None):
         isEsxi = not swPlatforms or \
            list(filter(lambda p:
               p.productLineID == SoftwarePlatform.PRODUCT_EMBEDDEDESX,
               swPlatforms))
         if isEsxi and kwargs["payloads"]:
            # With payload-update-only and payloads, check existence of an
            # overlay-order payload.
            # Ignore VIB objects without payloads, and ESXio VIBs due to
            # Quick Patch is not fully implemented on ESXio yet.
            cls._checkHasOverlayOrderPayload(kwargs["payloads"], vibnv)

      return kwargs

   def ToXml(self):
      """Serializes the object to XML.
            Returns: An etree.Element object.
      """
      xml = BaseVib.ToXml(self)

      elem = etree.SubElement(xml, "system-requires")
      elem.append(self.maintenancemode.ToXml())
      for hwplatform in self.hwplatforms:
         elem.append(hwplatform.ToXml())
      for swPlatform in self.swplatforms:
         elem.append(swPlatform.ToXml())

      if self.filelist:
         # Python sets are unordered, but we want filelist to be deterministic.
         filelist = list(self.filelist)
         filelist.sort()
         elem = etree.SubElement(xml, "file-list")
         for filename in filelist:
            etree.SubElement(elem, "file").text = filename

      elem = etree.SubElement(xml, "acceptance-level")
      elem.text = self.acceptancelevel

      elem = etree.SubElement(xml, "live-install-allowed")
      elem.text = self.liveinstallok and "true" or "false"

      elem = etree.SubElement(xml, "live-remove-allowed")
      elem.text = self.liveremoveok and "true" or "false"

      elem = etree.SubElement(xml, "cimom-restart")
      elem.text = self.cimomrestart and "true" or "false"

      elem = etree.SubElement(xml, "stateless-ready")
      elem.text = self.statelessready and "true" or "false"

      # Only generate <quick-patch> if it has sub contents for both
      # <resource-pool-definition>, <security-policy-dir> and <esx-scripts>,
      # or it has payload-update-only field set to true and no <esx-scripts>.
      if self.esxscripts and (not self.respooldef or not self.secpolicydir):
         msg = ("QuickPatch VIB %s has 'esxscripts' but not "
                "both 'respooldef' and 'secpolicydir'." % self.id)
         raise Errors.VibFormatError(self.id, msg)
      if not self.esxscripts and (self.respooldef or self.secpolicydir):
         msg = ("QuickPatch VIB %s has 'respooldef' or "
                "'secpolicydir' but no 'esxscripts'." % self.id)
         raise Errors.VibFormatError(self.id, msg)
      if self.payloadupdateonly and self.esxscripts:
         msg = ("QuickPatch VIB %s cannot have both "
                "'payloadupdateonly=true' and 'esxscripts'." % self.id)
         raise Errors.VibFormatError(self.id, msg)

      if self.payloadupdateonly:
         self._verifyIsVMwareCertified(self.acceptancelevel, self.vendor,
                                       self.id, "quick-patch")

         if self.payloads and \
            self.HasPlatform(SoftwarePlatform.PRODUCT_EMBEDDEDESX):
            # With payload-update-only and payloads, check existence of an
            # overlay-order payload.
            # Ignore VIB objects without payloads, and ESXio VIBs due to
            # Quick Patch is not fully implemented on ESXio yet.
            self._checkHasOverlayOrderPayload(self.payloads, self.id)

         elem = etree.SubElement(xml, "quick-patch")
         elem.set("payload-update-only", "true")
      elif self.esxscripts:
         self._verifyIsVMwareCertified(self.acceptancelevel, self.vendor,
                                       self.id, "quick-patch")
         elem = etree.SubElement(xml, "quick-patch")
         etree.SubElement(elem,
            "resource-pool-definition").text = self.respooldef
         etree.SubElement(elem, "security-policy-dir").text = self.secpolicydir
         scriptsElem = etree.SubElement(elem, "esx-scripts")
         for script in self.esxscripts:
            try:
               scriptsElem.append(script.ToXml())
            except ValueError as e:
               raise Errors.VibFormatError(self.id, str(e))

      # Enable this in OP
      #xml.append(self.resources.ToXml())

      etree.SubElement(xml, "overlay").text = self.overlay and "true" or "false"

      elem = etree.SubElement(xml, "payloads")
      for payload in self.payloads:
         elem.append(payload.ToXml())

      return xml

   def VerifyAcceptanceLevel(self, schemaonly=False):
      """Validates the acceptance level of this VIB against the policy for that
         acceptance level.
            Raises:
               * VibFormatError     - The VIB specifies an invalid acceptance
                                      level.
               * VibSigMissingError - The VIB is not signed.
               * VibSigFormatError  - The VIB signature does not have the
                                      appropriate format.
               * VibSigInvalidError - The VIB signature cannot be verified to
                                      be signed by a trusted CA.
               * VibSigDigestError  - The digest from the PKCS7 signature does
                                      not match a digest computed for the
                                      descriptor text.
      """
      try:
         policyobj = AcceptanceLevels.GetPolicy(self.acceptancelevel)
      except KeyError:
         msg = "Invalid acceptance level '%s'." % self.acceptancelevel
         raise Errors.VibFormatError(self.id, msg)

      if schemaonly:
         policyobj.VerifySchema(self)
      else:
         policyobj.Verify(self)

   def VerifySignature(self, verifyobj=None, cacerts=None, crls=None,
                       checkCertDates=False):
      """Validates the cryptographic signature of the descriptor.
            Parameters:
               * verifyobj      - An instance of VibSign.VibSigner that can be
                                  used to verify the signature.
               * cacerts        - A list of files to read CA certificates from.
                                  The CA certificates are used to validate the
                                  trust of the signer.
               * crls           - A list of files to read CRLs from.
               * checkCertDates - Whether to perform a stricter check on cert
                                  expiration and invalid Not-Before/Not-After
                                  dates.
            Returns: A VibSigner.SignerInfo object with information about the
                     signer.
            Exceptions:
               VibSigMissingError - The VIB is not signed.
               VibSigFormatError  - The VIB signature does not have the
                                    appropriate format.
               VibSigInvalidError - The VIB signature cannot be verified to be
                                    signed by a trusted CA.
               VibSigDigestError  - The digest from the PKCS7 signature does
                                    not match a digest computed for the
                                    descriptor text.
      """
      if not self._pkcs7:
         msg = "The VIB %s does not contain a signature." % (self.id)
         raise Errors.VibSigMissingError(self.id, msg)

      VibSign = loadVibSign()
      if verifyobj is None:
         if VibSign is None:
            msg = "Can not verify VIB signature: Required module missing."
            raise Errors.VibSigInvalidError(self.id, msg)
         try:
            verifyobj = VibSign.VibSigner(cacerts=cacerts, crls=crls)
         except Exception as e:
            msg = "Error creating VibSigner object: %s" % e
            raise Errors.VibSigInvalidError(self.id, msg)

      # For backward cert store compatibility, convention of storing signatures
      # is to put newer signature at the end. When checking signature, start
      # from the end and stop when a pass happens.
      for i in range(len(self._pkcs7) - 1, -1, -1):
         pkcs7 = self._pkcs7[i]
         try:
            logger.debug('Verifying VIB %s signature #%d', self.id, i + 1)
            try:
               signer = verifyobj.Verify(self._signeddesctext, pkcs7,
                                         checkCertDates)
            except TypeError:
               logger.warning("Ignoring the argument, checkCertDates")
               signer = verifyobj.Verify(self._signeddesctext, pkcs7)
            break
         except VibSign.PKCS7FormatError as e:
            err = Errors.VibSigFormatError(self.id, str(e))
         except VibSign.PKCS7DigestError as e:
            err = Errors.VibSigDigestError(self.id, str(e))
         except VibSign.PKCS7CertError as e:
            err = Errors.VibSigInvalidError(self.id, str(e))
         except Exception as e:
            msg = "Unexpected error validating VIB signature: %s" % e
            err = Errors.VibSigInvalidError(self.id, msg)
         # Last signature failed to verify, propagate error
         if i == 0:
            raise err
         # Log error and try next one
         else:
            logger.error('Failed to verify VIB signature #%d: %s',
                         i + 1, str(err))

      return signer

   def IterPayloads(self, checkDigests=False):
      """A Generator method that iterates over each payload of the VIB.
         Parameters:
            * checkDigests - If true, provide hashed payload object to perform
                             checksum verification.
         Returns:
            An iterator object. Each iteration returns a tuple (payload,
            fileobj):
             * payload - An instance of Payload describing the VIB payload
             * fileobj - a File-like object for reading from the payload;
                         supports read(), tell(), and close().
         Throws:
            * VibFormatError - if a corresponding file object cannot be found
                               for a payload.
      """
      # If we have any payloads with a fileobj already set, it might be
      # because one of the Installer classes specifically set it. So we default
      # to using that one first.
      for payload in self.payloads:
         # The partial VIB contains only Quick Patch related payloads, i.e.,
         # only the Quick Patch script payload, or Quick Patch script and patch
         # payloads. Skip the payloads that are not downloaded, including the
         # non-Quick Patch related ones.
         if self._isPartialVib:
            if not payload.isQuickPatchRelevant() or (payload.overlayorder and
                  not self._hasAllQpPatchPayloadFiles):
               continue

         if payload.fileobj is not None:
            fileObj = payload.fileobj
         elif self._arfile:
            fileObj = None
            for info, fobj in self._arfile:
               if info.filename == payload.name:
                  fileObj = fobj
                  break
            if fileObj is None:
               msg = ("Could not find file object for payload '%s'." %
                      payload.name)
               raise Errors.VibFormatError(self.id, msg)
         else:
            continue

         if checkDigests:
            fileObj = wrapPayloadReadFileobj(payload, fileObj, decompress=False,
                                             checkdigest=True)
         yield payload, fileObj

   def CheckPayloadDigests(self):
      """Iterate over all the payloads in this VIB, and verifies checksums for
      these payloads.
      """
      for payload, fobj in self.IterPayloads(checkDigests=True):
         try:
            while fobj.read(PAYLOAD_READ_CHUNKSIZE):
               pass
         except HashError as e:
            raise Errors.VibPayloadDigestError(self.id, payload.name, str(e))
         finally:
            fobj.close()

   @classmethod
   def FromFile(cls, f, schema=None):
      """Constructs an ArFileVib instance from a File object stream.
         Reads in the descriptor and optional signature, moving the
         file pointer to the first payload member.

         Parameters:
            * f - a File-like object supporting read() and close() methods,
                  or the path to a VIB file to read from.
            * schema - A file path to an XML schema for validation; must be in
                  .rng, .xsd, or .dtd format.  If None is passed in, then
                  schema validation is skipped.
         Exceptions:
            VibFormatError - AR members unexpected or descriptor XML invalid
                             or fails schema validation
      """
      if isString(f):
         srcfile = f
         try:
            fileobj = open(f, 'rb')
         except Exception as e:
            raise Errors.VibFormatError(f, str(e))
      else:
         fileobj = f
         if hasattr(f, 'name'):
            srcfile = f.name
         elif hasattr(f, 'url'):  #URLGrabberFileObject has url attribute
            srcfile = f.url
         else:
            srcfile = str(f)
      try:
         try:
            ar = ArFile.ArFile(fileobj=fileobj)
            info, memberfp = ar.next()
         except ArFile.ArError:
            raise Errors.VibFormatError(srcfile, "Bad VIB archive header")
         except StopIteration:
            raise Errors.VibFormatError(srcfile, "VIB archive is too short")
         except Exception as e:
            raise Errors.VibFormatError(srcfile, "Unable to open VIB archive "
                                    "in streaming mode: %s" % (str(e)))

         # read in descriptor
         if info.filename != 'descriptor.xml':
            if info.filename == 'debian-binary':
               msg = ("This VIB is a ESX 4.x VIB and is not supported on "
                      "this version of ESX.")
            else:
               msg = ("This VIB has an unsupported format.  The first member "
                      "found was %s, but descriptor.xml was expected." %
                      (info.filename))
            raise Errors.VibFormatError(srcfile, msg)

         try:
            desctext = memberfp.read()
            if sys.version_info[0] >= 3:
               desctext = desctext.decode()
         except Exception as e:
            raise Errors.VibFormatError(srcfile,
               "Unable to read VIB descriptor: %s" % (str(e)))
         if schema:
            vib = cls.FromXml(desctext, schema=schema)
         else:
            vib = cls.FromXml(desctext, validate=False)
         # preserve original descriptor
         vib._signeddesctext = desctext
         vib._arfile = ar
         if hasattr(fileobj, "name"):
            vib._fileorigin = fileobj.name

         # read in signature, if present
         try:
            info, memberfp = ar.next()
         except ArFile.ArError:
            raise Errors.VibFormatError(srcfile, "Bad VIB archive header")
         except StopIteration:
            raise Errors.VibFormatError(srcfile, "VIB archive is too short")
         except Exception as e:
            msg = "Unable to read VIB archive: %s" % str(e)
            raise Errors.VibFormatError(srcfile, msg)

         if info.filename != SIGN_PKCS:
            raise Errors.VibFormatError(srcfile,
               "Expected second member of Vib file to be sig.pkcs7, but found "
               "%s instead." % (info.filename))
         if info.size:
            try:
               vib.SetSignature(memberfp.read())
            except Exception as e:
               raise Errors.VibFormatError(srcfile,
                  "Unable to read VIB signature: %s" % (str(e)))
         else:
            vib.ClearSignature()

      except (Errors.VibFormatError, Errors.VibIOError,
              Errors.VibValidationError):
         if isString(f):
            fileobj.close()
         raise

      return vib

   @classmethod
   def GetVibFromPartialArFileVib(cls, parArFileVib):
      """Constructs an ArFileVib instance using PartialArFileVib object.
         It reads the descriptor and signature of the VIB from the vibContents
         attribute of the PartialArFileVib object.
         Parameters:
            * parArFileVib: - A PartialArFileVib object containing the partial
                              VIB payloads information.
      """
      descInfo, desctext = parArFileVib.vibContents[0]
      if descInfo.filename != 'descriptor.xml':
         msg = ("Invalid member %s found instead of descriptor.xml" %
                 (descInfo.filename))
         raise Errors.VibFormatError(parArFileVib._filename, msg)

      try:
         desctext = desctext.decode()
      except Exception as e:
         raise Errors.VibFormatError(parArFileVib._filename, "Unable to read"
                                     " VIB descriptor: %s" % (str(e)))

      vib = cls.FromXml(desctext)
      vib._signeddesctext = desctext
      vib._isPartialVib = True

      signInfo, signtext = parArFileVib.vibContents[1]
      if signInfo.filename != SIGN_PKCS:
         raise Errors.VibFormatError(
               parArFileVib._filename,
               "Invalid member %s found instead of sig.pkcs7" %
               (signInfo.filename))
      if signInfo.size:
         vib.SetSignature(signtext)
      else:
         vib.ClearSignature()
      return vib

   def OpenFile(self, f):
      """Associates a file object with the VIB.
            Paramaters:
               * f - A string giving a file path, or a file-like object.
            Raises:
               * VibFormatError - If the file content is not in the expected
                                  format.
            Note: This method does not implicitly call VerifySignature(). This
                  may be done, however, after this method call returns.
      """
      new = self.FromFile(f)
      for attr in self.ATTRS_TO_VERIFY + self.ATTRS_TO_COPY:
         expected = getattr(self, attr)
         found = getattr(new, attr)
         if expected != found:
            msg = ("Error opening file object for VIB '%s': Expected value "
                   "'%s' for attribute '%s', but found value '%s'."
                   % (self.id, expected, attr, found))
            raise Errors.VibFormatError(str(f), msg)

      self._arfile = new._arfile
      self._pkcs7 = new._pkcs7
      self._signeddesctext = new._signeddesctext

   def AddPayload(self, payloadobj, filepath, xzip=None, tmpDir=None,
                  objcopy=None, zstdPath=None):
      """Adds a payload at filepath to the VIB.

         If the payload name does not match one of the existing payloads,
         the payload will be added to a proper place to make the overall
         payload list in special sorted order: qp-script, qp-patch, normal,
         where qp-patch group is sorted by overlayorder, and normal group is
         by name. Both groups are in ascending order. We only have one
         qp-script payload if it exists. Note that only a quick patch VIB will
         have script and patch payloads.

         If the payload to add matches an existing payload, and does not have
         type conflict, for qp-script/normal types, or qp-patch type with the
         same overlayorder, that payload will be replaced in the same archive
         position when WriteVibFile() is called; for qp-patch type with
         different overlayorders, the payload to add will be inserted to a
         proper place mentioned above, and the old payload will be deleted.
         The size and fileobj attributes of the payloadobj will be modified.

         Parameters:
            * payloadobj   - An instance of Payload.
            * filepath     - The file path to the payload binary to add.
            * xzip         - Whether the payload is xzip compressed. This
                             should *only* be used during ESXi build.
            * tmpDir       - The temporary directory where xz uncompression
                             or zstd decompression takes place. Only to be
                             used during ESXi build.
            * objcopy      - Only used during build time for vmkboot payload.
                             If path is given, extract text segment of the
                             payload using objcopy and calculate txt-mle hash.
            * zstdPath     - The path to the zstd binary. This should *only*
                             be used during ESXi build.
         Raises:
            ValueError - Two payloads have the same name but different types;
                         or adding a script payload when there is already one.
            VibIOError - Unable to access payload or get file size.
            VibSigningError - Calculating hash for payload failed.
      """
      # check the payload file exists
      try:
         payloadobj.fileobj = open(filepath, 'rb')
         if payloadobj.payloadtype in Payload.ALL_GZIP_TYPES:
            # Ensure GZIP header is reproducible (created via `EsxGzip`).
            gzHeader = payloadobj.fileobj.read(len(EsxGzip.ESX_GZIP_HEADER))
            if gzHeader != EsxGzip.ESX_GZIP_HEADER:
               raise Errors.VibFormatError(
                  self.id,
                  "Payload file {!r} of VIB {!r} does not have valid EsxGzip "
                  "header: {!r} != {!r}"
                  .format(filepath, self.id, gzHeader, EsxGzip.ESX_GZIP_HEADER),
               )
            payloadobj.fileobj.seek(0)
      except IOError:
         raise Errors.VibIOError(filepath, "Unable to access payload")

      # Add file size
      try:
         payloadobj.size = os.stat(filepath).st_size
      except Exception:
         raise Errors.VibIOError(filepath, "Unable to get file size")

      try:
         # XXX: Backward compatibility hack
         # Checksum type is confined to sha-256 and sha-1 in ESXi 6.0 and
         # before. By putting new checksums at the start of list, regular
         # sha-256 without special verify process will be returned by
         # GetPreferredChecksum() for payload verification, otherwise VIB
         # install for new VIBs on older hosts will fail.

         # Produce txt-mle checksum for vmkboot during build
         if objcopy is not None:
            cksum_obj = getTxtMleChecksum(payloadobj, objcopy, tmpDir,
                                          zstdPath=zstdPath)
            payloadobj.checksums.append(cksum_obj)
            payloadobj.fileobj.seek(0)

         cktypes = [("sha-256", "gunzip"), ("sha-256", ""), ("sha-1", "gunzip")]
         for cksum_type, verifier_type in cktypes:
            decompress = (verifier_type == "gunzip")
            if decompress and payloadobj.payloadtype not in \
                  Payload.ALL_GZIP_TYPES:
               # "gunzip" verifier only makes sense for things that are
               # actually gzip'ed.
               continue
            checksum, bytesHashed = \
               calculatePayloadChecksumAndSize(payloadobj,
                  payloadobj.fileobj, cksum_type, decompress, tmpDir=tmpDir,
                  zstdPath=zstdPath)
            cksum_obj = Checksum(cksum_type, checksum, verifier_type)
            payloadobj.checksums.append(cksum_obj)
            payloadobj.fileobj.seek(0)
            if decompress:
               # When decompress is True, bytesHashed is the uncompressed size.
               payloadobj.uncompressedsize = bytesHashed
      except Exception as e:
         msg = ("Error calculating hash for payload %s: %s" %
                (payloadobj.name, e))
         raise Errors.VibSigningError(self.id, msg)

      if payloadobj.payloadtype in Payload.TARDISK_TYPES:
         tar = None
         try:
            tar = getTarFromPayload(payloadobj, xzip, tmpDir=tmpDir,
                                    zstdPath=zstdPath)

            for member in tar.getmembers():
               if member.type != tarfile.DIRTYPE:
                  self.filelist.add(member.name)

                  # If the file is a config schema, extract it and add
                  # the swtag based on the schema's metadata.
                  if member.name.startswith(CONFIG_SCHEMA_DIR):
                     extractDir = tempfile.mkdtemp(dir=tmpDir)
                     try:
                        tar.extract(member, path=extractDir)
                        filePath = os.path.join(extractDir, member.name)
                        csTag = ConfigSchemaSoftwareTag.FromPayloadFile(
                                                               filePath,
                                                               payloadobj.name,
                                                               member.name)
                     finally:
                        shutil.rmtree(extractDir)
                     self.swtags.append(csTag.ToString())

            payloadobj.fileobj.seek(0)
         finally:
            if tar is not None:
               tar.close()
      # After scanning payload contents, insert the payload object properly.
      self._InsertPayload(payloadobj)

   def _InsertPayload(self, payloadobj):
      """Insert a payload object at a proper place and keep the overall payload
         list in special sorted order: qp-script, qp-patch, normal, where
         qp-patch group is sorted by overlayorder, and normal group is by name.
         Both groups are in ascending order. This helper method will be used
         inside AddPayload right after the payload is scanned for its contents.
         Parameters:
            * payloadobj - an instance of Payload
      """
      # When self.payloads is empty, just simply append.
      if not len(self.payloads):
         self.payloads.append(payloadobj)
         return

      # When the payload to add matches an existing payload, check whether to
      # do replacement.
      for i in range(len(self.payloads)):
         if self.payloads[i].name == payloadobj.name:
            oldType = self.payloads[i].payloadcategory
            newType = payloadobj.payloadcategory
            if oldType != newType:
               # Avoid replacement between different payload types.
               raise ValueError("Two payloads with the same name '%s' have "
                  "unequal types '%s' != '%s'." % (payloadobj.name,
                  oldType, newType))

            if newType == Payload.PL_TYPE_QP_PATCH and \
                  self.payloads[i].overlayorder != payloadobj.overlayorder:
               # If both types are Payload.PL_TYPE_QP_PATCH but with different
               # overlayorder, we need a new place for the new payload.
               # Delete the old payload from the list, and insert the new one
               # in later logics.
               self.payloads.pop(i)
               break
            else:
               # If both types are Payload.PL_TYPE_QP_SCRIPT or
               # Payload.PL_TYPE_NORMAL, or Payload.PL_TYPE_QP_PATCH with the
               # same overlayorder, we can do a simple replacement.
               self.payloads[i] = payloadobj
               return

      # The payload to add is a qp-script payload.
      if payloadobj.payloadcategory == Payload.PL_TYPE_QP_SCRIPT:
         # qp-script payload should be in the front.
         if self.payloads[0].payloadcategory == Payload.PL_TYPE_QP_SCRIPT:
            # There is already a qp-script payload in the payload list, so we
            # cannot have one more with a different payload name.
            raise ValueError("Cannot add payload '%s': already have payload "
               "'%s' with Quick Patch scripts." % (payloadobj.name,
               self.payloads[0].name))
         self.payloads.insert(0, payloadobj)
         return

      # The payload to add is a qp-patch or normal payload.
      insertIdx = -1
      for i, curPayload in enumerate(self.payloads):
         if curPayload.payloadcategory == Payload.PL_TYPE_QP_SCRIPT:
            continue
         if payloadobj.payloadcategory == Payload.PL_TYPE_QP_PATCH:
            # The payload to add is with qp-patch type.
            if curPayload.payloadcategory == Payload.PL_TYPE_QP_PATCH:
               if payloadobj.overlayorder < curPayload.overlayorder:
                  insertIdx = i
                  break
            else:
               # The payload to add will be the first ever qp-patch payload,
               # or the last one in the list with the highest overlay-order.
               insertIdx = i
               break
         else:
            # The payload to add is with normal type.
            if curPayload.payloadcategory == Payload.PL_TYPE_NORMAL:
               if payloadobj.name < curPayload.name:
                  insertIdx = i
                  break

      if insertIdx == -1:
         # The payload to add will be the first normal payload, or the last
         # normal payload till now; or currently the payload list contains
         # only the qp-script payload, and the payload to add is a
         # qp-patch/normal payload. Append it to the end of the payload list.
         self.payloads.append(payloadobj)
      else:
         # Insert the payload to the proper place calculated above.
         self.payloads.insert(insertIdx, payloadobj)

   def AddQuickPatchEsxScript(self, quickPatchScript):
      """Adds a ESXi quick patch script to the VIB.
      """
      if not isinstance(quickPatchScript, QuickPatchScript):
         raise TypeError('Invalid object type. A QuickPatchScript type input '
                         'is required')
      for script in self.esxscripts:
         if script.path == quickPatchScript.path:
            raise ValueError('A script with path %s already exists'
                             % script.path)
      self.esxscripts.append(quickPatchScript)

   def GetDescriptorText(self):
      """Gets descriptor.xml text as a string.

         This function should be called to get the descriptor text
         before generating a signature for the vib
      """
      if sys.version_info[0] >= 3:
         encoding = 'unicode'
      else:
         encoding = 'us-ascii'

      try:
         self._signeddesctext = etree.tostring(self.ToXml(), encoding=encoding)
      except Exception as e:
         msg = "Error serializing VIB metadata to XML: %s" % e
         raise Errors.VibFormatError(self.id, msg)
      return self._signeddesctext

   def AddSignatureText(self, signaturetext):
      """Adds signature data to the VIB. Descriptor is not validated against
         the signature; it is the caller's responsibility to ensure that the
         object is not modified between calling GetDescriptorText and
         AddSignatureText, or between AddSignatureText and WriteVibFile.
            Parameters:
               * signaturetext - A string containing a PKCS7 signature of the
                                 descriptor.xml.
      """
      if sys.version_info[0] >= 3 and isinstance(signaturetext, str):
         signaturetext = signaturetext.encode()
      # Signature text is appended directly to self._pkcs7 list
      self._pkcs7.append(signaturetext.strip(b'\n'))

   def AddSignature(self, signerkey, signercert, extracerts=None):
      """Adds signature data to a VIB.
            Parameters:
               * signerkey  - A string giving the path name to the signer's
                              private key. Must be in PEM format.
               * signercert - A string giving the path name to the signer's
                              certificate. Must be in PEM format.
               * extracerts - A list of strings giving file names. Each file
                              name is searched for additional certificates to
                              add to the PKCS7 data.
            Returns: None
            Raises:
               * Errors.VibSigningError - on any problem generating the
                                          digest or signing it.
               * Errors.VibFormatError  - The descriptor data could not be
                                          serialized to a string.
            Notes: Any changes to the object's attributes which are stored in
                   descriptor.xml metadata will invalidate the signature, and
                   require calling this method again.
      """
      VibSign = loadVibSign()
      if VibSign is None:
         msg = "Do not have required signing function."
         raise Errors.VibSigningError(self.id, msg)

      desctext = self.GetDescriptorText()

      try:
         signer = VibSign.VibSigner(signerkey=signerkey,
                                    signercert=signercert,
                                    extracerts=extracerts)
         self.AddSignatureText(signer.Sign(desctext))
      except Exception as e:
         msg = "Error signing VIB: %s" % e
         raise Errors.VibSigningError(self.id, msg)

   def WriteVibFile(self, destfile, payloads=None):
      """Writes out a VIB to the file path destfile.  The payloads are written
         according to the order in the payloads attribute.
         If the payload has a fileobj, it tries to read the data from
         fileobj.  If fileobj is None, see if we can read it
         from the current AR archive.   The current file position
         must be at the first payload when this method is called, which means
         that after a WriteVibFile in which payloads are copied from the
         current AR archive, it cannot be called again.
         Note that the caller must make sure destfile is not the same as the
         source file.

         To create a VIB archive from scratch:
            vib = Vib.ArFileVib(name='myvib', ...)
            p1 = Vib.Payload(name='abc.tgz', payloadtype=...)
            vib.AddPayload(p1, filepath)
            vib.WriteVibFile(...)

         To add a payload to an existing VIB:
            vib = Vib.ArFileVib.FromFile('/path/to/my.vib')
            vib.AddPayload(p1, filepath)
            [any other changes to the VIB object]
            vib.WriteVibFile(...)

         To replace a payload, use the AddPayload() method with one of the
         existing payload names.  This populates the fileobj attribute and
         causes this method to write the payload from a file instead of from
         the current archive.

         To delete a payload, remove the payload from payloads list before
         invoking this method.

         Parameters:
            * destfile - The file path to write a VIB to.
            * payloads - A list of payloads that should be included in the VIB
         Exceptions:
            VibIOError - a payload instance cannot be sourced: fileobj is not
                         a file and the corresponding payload cannot be found
                         in the current AR archive (or there is no archive)
      """
      try:
         ar = ArFile.ArFile(name=destfile, mode='wb')

         # Write out descriptor
         # TODO: investigate when original descriptor is stored as bytes and
         # correct to string.
         if self._signeddesctext:
            desctext = (self._signeddesctext.encode()
                        if isString(self._signeddesctext)
                        else self._signeddesctext)
         else:
            desctext = etree.tostring(self.ToXml())
         ar.Writestr(ArFile.ArInfo("descriptor.xml"), desctext)

         # Write out signature
         ar.Writestr(ArFile.ArInfo("sig.pkcs7"), self.GetSignature())

         if QUICKPATCH_STAGE1_ENABLED and self._isPartialVib:
            for p in payloads:
               header, data = p
               ar.Writestr(header, data)
         else:
            # Write out the payloads in the payloads attribute
            # If the payload has a fileobj, try to read the data from
            # fileobj.  If fileobj is None, see if we can read it
            # from the current AR archive.   The current file position
            # must be at the first payload for the above to work.
            for payload, sfp in self.IterPayloads():
               ar.Writefile(ArFile.ArInfo(payload.name, size=payload.size), sfp)
               sfp.close()

         ar.Close()
      except (EnvironmentError, ArFile.ArError) as e:
         raise Errors.VibIOError(destfile, "Error writing VIB file: %s"
                                 % (str(e)))

   def GetFileOrigin(self):
      """Gets the original vib file name."""
      return self._fileorigin

   def GetModifiedConf(self):
      """ Gets all the config files which are modified in the system from a vib.
      """
      modifiedconf = []
      for fn in self.filelist:
         normalfile = PathUtils.CustomNormPath('/' + fn)
         fn_path = os.path.dirname(normalfile)
         fn_name = os.path.basename(normalfile)
         fn_name = '.#%s' % fn_name
         branch_fn = os.path.join(fn_path, fn_name)
         # if original file exists, .# file exists,
         # and original file has sticky bit on
         if os.path.isfile(normalfile) and \
               os.path.isfile(branch_fn) and \
               os.stat(normalfile).st_mode & stat.S_ISVTX:
            modifiedconf.append(fn)
      return modifiedconf

   def GetConfigSchemaTag(self, filterEsxioSchemas=True):
      """Returns a ConfigSchemaSoftwareTag objects representing this
         one config schema software tag.

         filterEsxioSchemas: The default behavior is to not return
            schema tag of vibs built for esxio software platform.
            When parameter is provided as False, it will return
            schema tag associated with this vib.
      """
      if (filterEsxioSchemas and
          not self.HasPlatform(SoftwarePlatform.PRODUCT_EMBEDDEDESX)):
         return None

      csTags = [ConfigSchemaSoftwareTag.FromString(s) for s in self.swtags
                if s.startswith(ConfigSchemaSoftwareTag.CONFIG_SCHEMA_MAGIC)]
      if len(csTags) > 1:
         raise ValueError('%u config schema tags found, expect at most 1'
                          % len(csTags))
      return csTags[0] if csTags else None

   def GetPlatformProductLineIDs(self, fillDefaultValue=False):
      """Returns a list of productLineID of this VIB if available, otherwise,
         depending on the value of fillDefaultValue, returns default embeddedEsx
         platform or an empty list.
      """
      if self.swplatforms:
         pids = list()
         # Preserve order and remove duplicate
         for sp in self.swplatforms:
            if sp.productLineID not in pids:
              pids.append(sp.productLineID)
         return pids
      elif fillDefaultValue:
         return [SoftwarePlatform.PRODUCT_EMBEDDEDESX]
      else:
         return []

   def HasPlatform(self, platform):
      """Checks if the Vib is intended for given software platform.
      """
      # Backward compatibility for VIBs without platform info, assume they
      # are all for embeddedEsx.
      if platform == SoftwarePlatform.PRODUCT_EMBEDDEDESX and \
         not self.swplatforms:
         return True

      for sp in self.swplatforms:
         if sp.productLineID == platform:
            return True

      return False

   def GetQuickPatchScriptsByType(self):
      """Returns a tuple of scan, apply-prepare and apply-published
         QuickPatchScript objects for this vib.
      """
      scanScripts = []
      applyPrepareScripts = []
      applyPublishScripts = []

      for script in self.esxscripts:
         if script.scriptType == QuickPatchScript.QP_SCRIPT_TYPE_SCAN:
            scanScripts.append(script)
         elif script.scriptType == \
              QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PREPARE:
            applyPrepareScripts.append(script)
         elif script.scriptType == \
              QuickPatchScript.QP_SCRIPT_TYPE_APPLY_PUBLISHED:
            applyPublishScripts.append(script)
         else:
            # Just log a warning and not error out
            logger.warning("Invalid script %s of type %s found in the vib",
                           script.name, script.scriptType)
            continue
      return scanScripts, applyPrepareScripts, applyPublishScripts

   def Close(self):
      if self._arfile:
         self._arfile.Close()
         self._arfile = None

class _ZstdReadFileObject(io.BufferedReader):
   """A special read file object returned after ZSTD decompression. The usage
      and input parameters are similar to open().
      ZSTD decompression always returns a new temp file which must be closed
      and explicitly removed after use. This class overrides the standard
      close() which removes the file. This allows the temp file to be removed
      without requiring the caller of wrapPayloadReadFileobj() to check
      whether the file object is a temp file.
   """
   def __init__(self, _file, **kwargs):
      fileObj = open(_file, mode='rb', **kwargs)
      super().__init__(fileObj.raw)
      self.fileObj = fileObj

   def close(self):
      # The following line would already close the raw object and no need to
      # close self.fileObj explicitly.
      super().close()
      os.remove(self.fileObj.name)

class _ZstdWriteFileObject(io.BufferedWriter):
   """A special write file object returned after ZSTD compression.
      In weasel, for early bootmodules, we need to append the signature blob
      right after the zstd layer but before the gzip layer. This brings up the
      need to have this special file object, since we need to know both gzip
      and zstd compressed file objects.
      Parameters:
         * zstdFileObj - The ZstdCompressionWriter instance.
         * gzFileObj - The GzipFile instance.
         * sigBlob - The contents of the sig blob in bytes, default to None.
                     It will be appended into the gzip layer after the zstd
                     layer is closed only when it is not empty.
   """
   def __init__(self, zstdFileObj, gzFileObj, sigBlob=None):
      super().__init__(zstdFileObj)
      self.gzFileObj = gzFileObj
      self.sigBlob = sigBlob

   def close(self):
      # Close the zstd layer.
      super().close()
      # Write the signature blob if exists.
      if self.sigBlob:
         self.gzFileObj.write(self.sigBlob)
      # Close the gzip layer.
      self.gzFileObj.close()

def _truncateSigBlob(inputFileObj, payloadName):
   """Helper function to truncate the ending signature bytes if the payload
      is signed. This is needed by zstd decompression.
      We need to manually trim the trailing bytes, since there are no command
      line options to use in zstd (like `--single-stream` in xz).
      ZSTD_TODO: find out if python zstandard can ignore this block when the
      compression/decompression at build time fully turns into the python
      wrapper.
   """
   # IMPORTANT: The schema magic must match what is listed in
   # efiboot/mboot/secure.c, bora/scons/vtools/common/sign-payload.py, or
   # bora/bazel/esx/sign_vib_payload.py.
   # TODO: Refactor the comment above once SCons usage is fully deprecated.
   SCHEMA_MAGIC = 0x1abe11ed
   try:
      # 1) Get the last 8 bytes (trailerEndSize). Within these bytes, the
      # first four are the length of the actual signature file + 16 byte
      # keyid, and the last four are SCHEMA_MAGIC.
      trailerEndFmt = "<II"
      trailerEndSize = struct.calcsize(trailerEndFmt)
      inputFileObj.seek(-trailerEndSize, os.SEEK_END)
      keyIDAndSigLength, schemaMagic = struct.unpack(trailerEndFmt,
                                       inputFileObj.read(trailerEndSize))
      if schemaMagic != SCHEMA_MAGIC:
         logger.debug("Skip truncating since the payload '%s' is unsigned.",
                      payloadName)
      else:
         # 2) Get the location where the signature begins.
         # Seek from behind 12 bytes (8 bytes trailerEndSize + 4 bytes
         # trailerStartSize) for the schemaVersion/keyIDAndSigLength/
         # schemaMagic fields (each takes 4 bytes), and the size of the
         # keyID + signature (keyIDAndSigLength).
         trailerStartFmt = "<I"
         trailerStartSize = struct.calcsize(trailerStartFmt)
         offset = -trailerEndSize - trailerStartSize - keyIDAndSigLength
         # 3) Get schemaVersion from the first four bytes. This is just to
         # make sure the structure is as expected and we do not enforce the
         # schema version.
         inputFileObj.seek(offset, os.SEEK_END)
         struct.unpack(trailerStartFmt, inputFileObj.read(trailerStartSize))
         # 4) Remove the trailing sinagure bytes.
         inputFileObj.seek(offset, os.SEEK_END)
         endPos = inputFileObj.tell()
         inputFileObj.truncate(endPos)
      inputFileObj.seek(0)
   except Exception as e:
      msg = "Failed to truncate payload '%s'. Reason: %s" % (payloadName,
                                                             str(e))
      raise ValueError(msg)

def _wrapZstdPayloadReadFileObj(payload, fileObj, tmpDir=None,
                                zstdPath=None):
   """For a vzstd payload, get the decompressed object. We will first try to
      import the zstd Python library (only available at runtime), then try with
      the zstd binary if the import fails (build time usage). If neither way
      works, raise an exception.
      Parameters:
         * payload - The payload object.
         * fileObj - The file object associated with the payload.
         * tmpDir  - The temporary directory where zstd decompression takes
                     place. Only to be used during ESXi build.
         * zstdPath - The path to the zstd binary. Only to be used during ESXi
                      build.
      Returns:
         * A zstd decompressed object after ungzipped.
      Raises:
         * ValueError - The payload is not vzstd type;
                        An exception is raised when fetching the zstandard
                        library;
                        Failed to truncate a signed payload.
         * RuntimeError - An exception is raised when decompressing the
                          payload using the zstd binary.
   """
   def _decompressionByZstdBinary(inputPath, outputPath, zstdPath):
      '''Helper function to decompress a file object by using the zstd binary.
      '''
      # Run the command.
      subprocess.check_call(
         '{zstd} -d {inFile} -o {outFile}'.format(zstd=zstdPath,
            inFile=inputPath, outFile=outputPath),
         # Use a shell: unfortunately, the build still passes
         # `LD_LIBRARY_PATH=...` as part of the `--zstd` argument.
         shell=True)

   if payload.payloadtype != Payload.TYPE_VZSTD:
      raise ValueError("Payload '%s' is '%s' type thus cannot be zstd "
         "compressed." % (payload.name, payload.payloadtype))

   # Un-gzip the file object first.
   fileObj = EsxGzip.GzipFile(mode='rb', fileobj=fileObj)

   # Temporarily save the file object.
   with tempfile.NamedTemporaryFile(dir=tmpDir) as tmpFile:
      tmpPath = tmpFile.name
      fileObj.seek(0)
      shutil.copyfileobj(fileObj, tmpFile, PAYLOAD_READ_CHUNKSIZE)

      # Truncate the ending signature bytes if the payload is signed.
      _truncateSigBlob(tmpFile, payload.name)

      outputPath = tmpPath + '.unzstd'
      # Then perform zstd decompression.
      # ZSTD_TODO: Reconsider whether to keep the binary approach after Python
      # zstandard wrapper is integrated for build.
      if zstdPath:
         # If zstdPath is explicitly given, use zstd binary first. Usually this
         # is build time usage only.
         _decompressionByZstdBinary(tmpPath, outputPath, zstdPath)
      else:
         # zstdPath is None, check whether zstandard library is supported. This
         # is runtime usage only.
         if not HAS_ZSTANDARD_PYTHON:
            raise ValueError("Payload '%s' is '%s' type, but no zstandard "
               "library or zstd binary is found." % (payload.name,
               Payload.TYPE_VZSTD))
         dctx = zstd.ZstdDecompressor()
         with zstd.open(tmpFile, mode='rb', dctx=dctx, closefd=True) as \
               unzstdFileObj:
            # zstd.open() with mode='rb' returns a ZstdDecompressionReader
            # object, which cannot seek backwards, thus converting to a normal
            # file object.
            with open(outputPath, 'wb') as f:
               shutil.copyfileobj(unzstdFileObj, f, PAYLOAD_READ_CHUNKSIZE)

      # Check file size to see if zstd decompression is fine.
      if os.path.getsize(outputPath) == 0:
         raise RuntimeError("Failed to zstd decompress payload '%s': "
                            "output is empty." % payload.name)

      # Create a _ZstdReadFileObject instance with the output file to return.
      resultFileObj = _ZstdReadFileObject(outputPath)

   return resultFileObj

def _wrapZstdPayloadWriteFileObj(payload, fileObj, sigBlob=None):
   """For a vzstd payload, get the compressed object. We will import and use
      zstd Python libraries. If the import fails, raise an exception.
      Parameters:
         * payload - The payload object.
         * fileObj - The file object associated with the payload.
         * sigBlob - The bytes contents of the signature blob of the signed
                     payload to append; default is None, i.e. do not append
                     any signature.
      Returns:
         * A zstd compressed object wrapped with a compression level 0
           gzip layer.
      Raises:
         * ValueError - The payload is not vzstd type;
                        An exception is raised when fetching the zstd Python
                        library.
   """
   if payload.payloadtype != Payload.TYPE_VZSTD:
      raise ValueError("Payload '%s' is '%s' type thus cannot be zstd "
         "compressed." % (payload.name, payload.payloadtype))

   # Check whether zstd is supported at runtime.
   if not HAS_ZSTANDARD_PYTHON:
      raise ValueError("Payload '%s' is '%s' type, but no zstandard library "
         "is found." % (payload.name, Payload.TYPE_VZSTD))

   # Perform zstd compression wrapped with gzip level 0.
   fileObj = EsxGzip.GzipFile(mode='wb', compresslevel=0,
                              fileobj=fileObj)
   cctx = zstd.ZstdCompressor(level=int(payload.compressionoption[
                              Payload.ZSTD_COMPRESSION_LEVEL]))
   resultFileObj = zstd.open(fileObj, mode='wb', cctx=cctx)

   return _ZstdWriteFileObject(resultFileObj, fileObj, sigBlob=sigBlob)

def wrapPayloadReadFileobj(payload, srcfObj, decompress, checkdigest,
                           tmpDir=None, zstdPath=None):
   '''Get a wrapped payload fileobj for decompression and/or checksum check
      during read, typically used when iterating a VIB with IterPayloads.
      Parameters:
         payload                - The Payload class object.
         srcfObj                - The read file object associated with the
                                  payload, returned after wrapping.
         decompress/checkdigest - True/False flags indicating decompress and
                                  checksum check requirements.
         tmpDir                 - The temporary directory where zstd
                                  decompression takes place. Only to be used
                                  during ESXi build when decompress is True.
         zstdPath               - The path to the zstd binary. Only to be used
                                  during ESXi build when decompress is True.
      Returns:
         A wrapped file object based on srcfObj.
      Scenarios:
         decompress             - return unzip stream
         checkdigest            - return a HashedStream verifying regular
                                  stream with regular checksum
         decompress/checkdigest - return an EsxGzip stream while using
                                  HashedStream to verify checksum of the
                                  original file

      Raises:
         ValueError - when the payload object does not have the needed checksum
   '''
   if checkdigest:
      # Verify complete payload checksum everytime.
      checksum = payload.GetPreferredChecksum(verifyprocess='')
      if not checksum:
         msg = 'Payload %s does not have a checksum' % payload.name
         raise ValueError(msg)
      srcfObj = HashedStream(srcfObj, checksum.checksum,
                             checksum.checksumtype.replace('-', ''))

   if decompress:
      if payload.payloadtype in Payload.GZIP0_TYPES:
         # vzstd will be unzstded after being ungzipped.
         srcfObj = _wrapZstdPayloadReadFileObj(payload, srcfObj,
                      tmpDir=tmpDir, zstdPath=zstdPath)
      elif payload.payloadtype in Payload.GZIP_TYPES:
         # boot and legacy vgz will only be ungzipped.
         srcfObj = EsxGzip.GzipFile(mode='rb', fileobj=srcfObj)

   return srcfObj

def wrapPayloadWriteFileobj(payload, destfObj, compress, sigBlob=None):
   '''Get a wrapped payload fileobj for compressing payload data, typically
      used when staging from /tardisks to the stage bootbank.
      Parameters:
         * payload  - The Payload class object.
         * destfObj - The write file object associated with the payload,
                      returned after wrapping.
         * compress - True/False flag indicating whether to compress.
         * sigBlob  - The bytes contents of the signature blob of the signed
                      payload to append; default is None, i.e. do not append
                      any signature. Only can be used by vzstd payloads.
      Returns:
         A wrapped file object based on destfObj.
   '''
   if compress:
      if payload.payloadtype in Payload.GZIP0_TYPES:
         # vzstd will proceed with zstd + level 0 gzip compression.
         destfObj = _wrapZstdPayloadWriteFileObj(payload, destfObj,
                                                 sigBlob=sigBlob)
      elif payload.payloadtype in Payload.GZIP_TYPES:
         # boot and legacy vgz will proceed with the original gzip
         # compression.
         destfObj = EsxGzip.GzipFile(mode='wb', fileobj=destfObj)

   return destfObj

def calculatePayloadChecksumAndSize(payload, srcfObj, hashAlgo, decompress,
                                    tmpDir=None, zstdPath=None):
   """Calculate checksum of a payload file object, return the checksum and
      bytes read.
      Parameters:
         payload    - Payload class object.
         srcfObj    - Payload file for read, will not be closed after read.
         hashAlgo   - Hashing algorithm to use.
         decompress - Whether to calculate decompressed checksum;
                      when set to True, wrapPayloadReadFileobj() is used to
                      provide the proper file object for decompression.
         tmpDir     - The temporary directory to use.
         zstdPath   - The path to the zstd binary.
      Return:
         A tuple of (digest of the file in hex, total bytes read from hash).
   """
   if decompress:
      srcfObj = wrapPayloadReadFileobj(payload, srcfObj, decompress=True,
                                       checkdigest=False, tmpDir=tmpDir,
                                       zstdPath=zstdPath)
   hs = HashedStream(srcfObj, method=hashAlgo.replace('-', ''))
   data = hs.read(PAYLOAD_READ_CHUNKSIZE)
   bytesHashed = len(data)
   while data:
      data = hs.read(PAYLOAD_READ_CHUNKSIZE)
      bytesHashed += len(data)

   if decompress:
      # This does not close the original srcfObj.
      srcfObj.close()

   return hs.hexdigest, bytesHashed

def getTxtMleChecksum(payloadobj, objcopy, tmpDir, zstdPath=None):
   """Calculate txt-mle checksum for the payload, returns a Checksum object.
      This function should be only called at build time for vmkboot payload
      specifically.
      Parameters:
         * Check AddPayload() for details.
   """
   # Decompress the payload
   with wrapPayloadReadFileobj(payloadobj, payloadobj.fileobj,
                               decompress=True, checkdigest=False,
                               tmpDir=tmpDir, zstdPath=zstdPath) as f:
      if payloadobj.payloadtype not in Payload.GZIP0_TYPES:
         with tempfile.NamedTemporaryFile(dir=tmpDir, delete=False) as \
               tmpExtract:
            shutil.copyfileobj(f, tmpExtract, PAYLOAD_READ_CHUNKSIZE)
            tmpExtract.flush()
      else:
         # When payloadobj.payloadtype is TYPE_VZSTD, a temp file is already
         # created and used inside wrapPayloadReadFileobj.
         tmpExtract = f

      with tempfile.NamedTemporaryFile(dir=tmpDir) as tmpText:
         # Dump text part of vmkboot
         subprocess.check_call([objcopy,
                                '--output-target=binary',
                                '--only-section=.text',
                                tmpExtract.name,
                                tmpText.name])
         checksum, _ = calculatePayloadChecksumAndSize(payloadobj,
                          tmpText, 'sha256', decompress=False)
         cksumObj = Checksum('sha-256', checksum, 'txt-mle')

   # The file tmpExtract.name has already been removed in "f.close()" if the
   # payload type is TYPE_VZSTD.
   if payloadobj.payloadtype not in Payload.GZIP0_TYPES:
      os.remove(tmpExtract.name)

   return cksumObj

def getTarFromPayload(payloadobj, xzip, tmpDir=None, zstdPath=None):
   ''' Return a given payload as a tarfile object.
       Caller needs to provide a temporary directory in case the payload
       is xzipped. It is caller's reponsibility to cleanup the directory.

       Parameter:
         xzip     - The path to the xzip binary.
         tmpDir   - A temporary diretory to use, if not given the system
                    default is used.
         zstdPath - The path to the zstd binary.
   '''
   DEFAULT_XZ = '/bin/xz'
   XZ_MAGIC = b'\xfd7zXZ\x00'

   if payloadobj.payloadtype == payloadobj.TYPE_VZSTD:
      src = wrapPayloadReadFileobj(payloadobj, payloadobj.fileobj,
               decompress=True, checkdigest=False, tmpDir=tmpDir,
               zstdPath=zstdPath)
      tar = VmTar.Open(fileobj=src, mode='r')
   elif payloadobj.payloadtype == payloadobj.TYPE_VGZ:
      src = wrapPayloadReadFileobj(payloadobj, payloadobj.fileobj,
                                   decompress=True, checkdigest=False)
      isXzipped = src.read(len(XZ_MAGIC)) == XZ_MAGIC
      # Seek back, the file object might be a .vib file and seeking an absolute
      # location is incorrect.
      src.seek(-len(XZ_MAGIC), 1)
      if isXzipped:
         # xzip payload, extract to a temporary file
         if xzip is None:
            if os.path.exists(DEFAULT_XZ):
               xzip = DEFAULT_XZ
            else:
               raise ValueError("Payload %s is xzipped, but no xzip was "
                                "provided" % payloadobj.name)

         # Order allows for the gunzipped file to be closed before the $xzip
         # command is executed. Fixes "truncated input" errors from XZ as
         # observed during HostSeeding, and likely PR 3214542.
         with tempfile.NamedTemporaryFile(dir=tmpDir, delete=False) as tarFd:
            with tempfile.NamedTemporaryFile(dir=tmpDir, delete=False) as gunzipFd:
               # Gzip decompress to gunzipFd and then xz decompress to tarFd.
               # gunzipFd file is deleted after use, while tarFd file is kept
               # in-place assuming tmpDir would be cleaned up afterwards.
               #
               # --stdin input to xz is not used since xz can close pipe before
               # receiving the appended signature after the compressed payload.
               shutil.copyfileobj(src, gunzipFd)

            subprocess.check_call(
               # Use `--single-stream` to decompress only the first XZ stream
               # (payload); silently ignore remaining data (signature).
               # (Other errors will still be reported.)
               '{xzip} --decompress --stdout --check=crc32 '
               '--single-stream {inFile}'.format(
                  xzip=xzip, inFile=gunzipFd.name,
               ),
               stdout=tarFd,
               # Use a shell: unfortunately, the build still passes
               # `LD_LIBRARY_PATH=...` as part of the `--xzip` argument.
               shell=True,
            )

            # Immediately delete that temporary file.
            os.remove(gunzipFd.name)

         # Check file size instead.
         if os.path.getsize(tarFd.name) == 0:
            raise RuntimeError('Failed to xz decompress payload %s'
                               % payloadobj.name)

         tar = VmTar.Open(name=tarFd.name, mode='r')
      else:
         tar = VmTar.Open(fileobj=src, mode='r')
   elif payloadobj.payloadtype == payloadobj.TYPE_TGZ:
      tar = tarfile.open(mode="r:gz", fileobj=payloadobj.fileobj,
                         format=tarfile.GNU_FORMAT)
   else:
      raise ValueError('Payload type %s is not a regular tar type payload'
                       % payloadobj.payloadtype)
   return tar

def getConfigSchema(vib, xzip, tmpDir=None, zstdPath=None):
   """Returns a config schema object from the VIB, or None when the VIB has
      no config schema. Requires the VIB object to be backed by the actual
      VIB file so payload can be extracted.
      Parameters: similar to getTarFromPayload().
   """
   csTag = vib.GetConfigSchemaTag()
   if csTag is None:
      return None
   for payload, fObj in vib.IterPayloads():
      if payload.name == csTag.vibPayload:
         payload.fileobj = fObj
         tar = getTarFromPayload(payload, xzip, tmpDir=tmpDir,
                                 zstdPath=zstdPath)
         extractDir = tempfile.mkdtemp(dir=tmpDir)
         try:
            tar.extract(csTag.payloadFilePath, path=extractDir)
            filePath = os.path.join(extractDir, csTag.payloadFilePath)
            cs = ConfigSchema.FromFile(filePath)
         finally:
            shutil.rmtree(extractDir)
         return cs
   raise RuntimeError('Unable to find payload %s/%s that contains a config '
                      'schema' % (vib.id, csTag.vibPayload))

def addVibExports(vib, schemaCollection, exportCollection, xzip, tmpDir=None,
                  zstdPath=None):
   """
   Add the exports from a VIB to the given collections.

   Parameters:
      * vib              - The VIB to extract the schemas/exports from.
      * schemaCollection - The collection to add configSchemas to.
      * exportCollection - The collection to add VIB exports to.
      * xzip             - The command to use for extracting an xz compressed
                           VIB.
      * tmpDir           - The temporary directory to use.
      * zstdPath         - The path to the zstd binary.
   """
   csTag = vib.GetConfigSchemaTag()
   if csTag is None:
      return

   tarMap = dict()
   extractDir = tempfile.mkdtemp(dir=tmpDir)
   def extractVibExports(tar):
      for name in tar.getnames():
         exportLoader = getLoaderForVibPath(csTag.schemaId, name)
         if not exportLoader:
            continue
         tar.extract(name, path=extractDir)
         exportPath = os.path.join(extractDir, name)
         with open(exportPath, 'rb') as fp:
            export = exportLoader(fp.read())
            exportCollection.AddVibExport(export)
   def getExtractedTar(payload):
      if payload.name not in tarMap:
         tarMap[payload.name] = getTarFromPayload(payload, xzip, tmpDir=tmpDir,
                                                  zstdPath=zstdPath)
      return tarMap[payload.name]

   try:
      for payload, fObj in vib.IterPayloads():
         payload.fileobj = fObj
         if payload.name == csTag.vibPayload:
            if schemaCollection is None:
               continue
            tar = getExtractedTar(payload)
            tar.extract(csTag.payloadFilePath, path=extractDir)
            filePath = os.path.join(extractDir, csTag.payloadFilePath)
            schemaCollection.AddConfigSchema(ConfigSchema.FromFile(filePath))
            extractVibExports(tar)
         elif payload.payloadtype in Payload.TARDISK_TYPES:
            tar = getExtractedTar(payload)
            extractVibExports(tar)
   finally:
      shutil.rmtree(extractDir)

def copyPayloadFileObj(payload, srcfObj, destfObj, compress=False,
                       decompress=False, checkdigest=False, checksize=False):
   """Copy contents from a payload file object to another file object.
      In compress mode, the destfObj will be compressing the data stream
      from srcfObj according to the payload type.
      In decompress mode, the srcfObj is decompressed according to the
      payload type and optional has its checksum verified during the write.
      In checkdigest mode, the srcfObj is copied to the destfObj with its
      checksum verified during the write.
      In checksize mode, the size of srcfObj is compared with the size field
      provided via descriptor to detect any tampering in VIB during
      installation. It is kept False as default as some existing payloads
      present on the host may have been gzipped using a different gzip tool
      than the build process, resulting in a different size.
   """
   if compress and decompress:
      raise ValueError('Only one of compress or decompress can be set')
   if compress:
      if checkdigest:
         # compress + checkdigest is not implemented now since the only
         # use case is to stage a tardisk from /tardisks and it was verified
         # before mounting.
         raise NotImplementedError('checkdigest is not implemented with '
                                   'compress mode')
      destfObj = wrapPayloadWriteFileobj(payload, destfObj, True)
   else:
      # decompress + checkdigest, or just checkdigest
      if isinstance(srcfObj, ArFile._FileInFile):
         size = srcfObj.size
      else:
         size = os.fstat(srcfObj.fileno()).st_size

      # Special case for payloads that encapsulate state.
      # These payloads can change. Hence their sizes
      # will not match the ones in the header.
      # TODO: Since OEMs can add additional useropts modules,
      # we will need to get this list from the kernel.
      if payload.name not in CHECKSUM_EXEMPT_PAYLOADS and size != payload.size:
         msg = "Payload %s size does not match with the one specified in " \
               "descriptor, '%s' != '%s'" % (payload.name, size, payload.size)
         if checksize:
            raise ValueError(msg)
         logger.warning(msg)

      srcfObj = wrapPayloadReadFileobj(payload, srcfObj, decompress,
                                       checkdigest)
   totalBytes = 0
   inBytes = srcfObj.read(PAYLOAD_READ_CHUNKSIZE)
   while inBytes:
      destfObj.write(inBytes)
      totalBytes += len(inBytes)
      inBytes = srcfObj.read(PAYLOAD_READ_CHUNKSIZE)
   logger.debug('Total of %d bytes were written.', totalBytes)

   # This does not close the original srcfObj/destfObj.
   if decompress:
      srcfObj.close()
   elif compress:
      destfObj.close()

def GetHostSoftwarePlatform():
   """ Get the host's software platform.
   """
   from . import IS_ESXIO
   if IS_ESXIO:
      return SoftwarePlatform.PRODUCT_ESXIO_ARM
   else:
      return SoftwarePlatform.PRODUCT_EMBEDDEDESX

BaseVib._vib_classes[BaseVib.TYPE_BOOTBANK] = ArFileVib
BaseVib._vib_classes[BaseVib.TYPE_META] = ArFileVib
BaseVib._vib_classes[BaseVib.TYPE_LOCKER] = ArFileVib
