#!/usr/bin/python
########################################################################
# Copyright (C) 2010-2018 VMWare, Inc.                                 #
# All Rights Reserved                                                  #
########################################################################

"A module providing classes for reading and writing ISO9660 file systems."

import datetime, errno, os, struct, sys, tempfile

if sys.version_info[0] >= 3:
   from io import BytesIO
else:
   from StringIO import StringIO as BytesIO

from .Misc import isString

SECTOR_SIZE = 2048
MAX_DIRECTORY_DEPTH = 8

# Chunk size for reads and writes when writing the individual files to
# the ISO image.
WRITE_READ_CHUNK_SIZE = 1024 * 1024

class Iso9660Error(Exception):
   pass

class ElToritoError(Iso9660Error):
   pass

class IsoTzInfo(datetime.tzinfo):
   def __init__(self, offset):
      self.offset = offset

   def utcoffset(self, dt):
      return self.offset

   def dst(self, dt):
      return None

def GetBothEndian16(data):
   """The ISO9660 standard specifies many fields which are expressed first in
      little-endian order, then in big-endian order. For example, the 16-bit
      integer 2048 is expressed as 0x00 0x08 0x08 0x00. This function converts
      such 16-bit values to an integer.
         Parameters:
            * data - A string of length 4.
         Returns: An integer value.
   """
   if sys.byteorder == "little":
      return struct.unpack("<H2x", data)[0]
   else:
      return struct.unpack(">2xH", data)[1]

def GetBothEndian32(data):
   """This function performs the same operation as GetBothEndian16, except for
      32-bit values.
         Parameters:
            * data - A string of length 8.
         Returns: An integer value.
   """
   if sys.byteorder == "little":
      return struct.unpack("<I4x", data)[0]
   else:
      return struct.unpack(">4xI", data)[1]

def MkBothEndian16(n):
   """This function is the inverse of GetBothEndian16.
         Parameters:
            * n - an integer value.
         Returns: A string of length 4.
   """
   return struct.pack("<H", n) + struct.pack(">H", n)

def MkBothEndian32(n):
   """This function is the inverse of GetBothEndian32.
         Parameters:
            * n - an integer value.
         Returns: A string of length 8.
   """
   return struct.pack("<I", n) + struct.pack(">I", n)

def RoundUp(n, multiple):
   """This function rounds an integer to the next multiple. For instance,
      if multiple is 100, then the function returns 100 for values 1-100, 200
      for values 101-200, etc.
         Parameters:
            * n        - An integer specifying the number to be rounded.
            * multiple - The multiple to round to.
         Returns: n rounded to the next multiple.
   """
   if n % multiple:
      return n + (multiple - n % multiple)
   return n

class VolumeDescriptor(object):
   "A base class for supported volume descriptor types."
   descriptor_types = dict()

   def __init__(self):
      self.type = None
      self.standard = "CD001"
      self.version = None

   @classmethod
   def FromStr(cls, data):
      """Returns a volume descriptor object from a data string.
            Parameters:
               * data - A string containing the volume descriptor data.
            Returns: An instance of a child class of VolumeDescriptor,
                     appropriate to the descriptor type.
            Raises:
               * Iso9660Error - If the data is too short or does not describe
                                a known standard or volume descriptor type.
      """
      if len(data) < SECTOR_SIZE:
         raise Iso9660Error("Volume Descriptor data length too short.")
      desctype = ord(data[0])
      standard = data[1:6]
      if standard != "CD001":
         raise Iso9660Error("Unknown Standard Identifier '%s'." % standard)
      elif desctype not in cls.descriptor_types:
         raise Iso9660Error("Unknown Volume Descriptor Type '%s'." % desctype)
      new = cls.descriptor_types[desctype]()
      new._FromStr(data)
      return new

   def ToStr(self):
      """Serializes the volume descriptor object to ISO9660 format.
            Returns: A string of length SECTOR_SIZE.
      """
      return self._ToStr()

   def _ToStr(self):
      return struct.pack("B", self.type) + b"CD001"

   @classmethod
   def FromFile(cls, f):
      """Returns a volume descriptor object from a file(-like) object.
            Parameters:
               * f - A file or file-like object.
            Returns: An instance of a child class of VolumeDescriptor.
      """
      data = f.read(SECTOR_SIZE)
      return cls.FromStr(data)

class BootRecord(VolumeDescriptor):
   "A class representing an ISO9660 boot record."
   def __init__(self):
      VolumeDescriptor.__init__(self)
      self.type = 0
      self.version = 1
      self.bootsystem = b""
      self.bootid = b""
      self.bootdata = b""

   def _FromStr(self, data):
      self.version = ord(data[6])
      if self.version != 1:
         msg = ("Unknown Descriptor Version for Boot Record: '%s'."
                % self.version)
         raise Iso9660Error(msg)
      self.bootsystem = data[7:39].rstrip()
      self.bootid = data[39:71].rstrip()
      self.bootdata = data[71:SECTOR_SIZE]

   def _ToStr(self):
      return b"".join((VolumeDescriptor._ToStr(self),
                      struct.pack("B", self.version),
                      self.bootsystem[:32].upper().ljust(32, b"\x20"),
                      self.bootid[:32].upper().ljust(32, b"\x20"),
                      self.bootdata[:1977].ljust(1977, b"\x20")))

class PrimaryVolumeDescriptor(VolumeDescriptor):
   """A class representing an ISO9660 Primary Volume Descriptor.
         Attributes:
            type                       - An integer giving the volume descriptor
                                         type. This is always 1 for the Primary
                                         Volume Descriptor.
            version                    - An integer giving the version of the
                                         standard the volume descriptor complies
                                         with. Always 1 for this implementation.
            systemid                   - A string of 32 bytes or less giving the
                                         content of the system ID field.
            volumeid                   - A string of 32 bytes or less giving the
                                         content of the volume ID field.
            volumespacesize            - The number of logical blocks comprising
                                         the entire volume content for this
                                         descriptor. (For all practical
                                         purposes, each logical block is SECTOR_SIZE
                                         bytes.)
            volumesetsize              - The number of volumes in the volume set
                                         to which this volume is a member.
            volumesequencenumber       - This volume's index within the volume
                                         set.
            blocksize                  - The size of each logical block on the
                                         volume. Only SECTOR_SIZE is supported.
            pathtablesize              - The size in bytes of either path table
                                         in this volume.
            lpathtablelocation         - The logical block number giving the
                                         location of the path table containing
                                         little-endian values.
            loptionalpathtablelocation - The location of an optional path table
                                         containing little-endian values.
            mpathtablelocation         - The location of the path table
                                         containing big-endian values.
            moptionalpathtablelocation - The location of the optional path table
                                         containing big-endian values.
            rootdirectory              - An instance of DirectoryRecord
                                         representing the volume's root
                                         directory.
            volumesetid                - A string of up to 128 characters
                                         specifying a name for the volume set
                                         to which this volume is a member.
            publisherid                - A string of up to 128 characters
                                         specifying the ID of the publisher.
            datapreparerid             - A string of up to 128 characters
                                         specifying the ID of the preparer.
            applicationid              - A string of up to 128 characters
                                         specifying the ID of the application.
            copyrightfileid            - A string giving the ID of a file on the
                                         volume containing copyright
                                         information.
            abstractfileid             - A string giving the ID of a file on the
                                         volume containing an abstract.
            bibliofileid               - A string giving the ID of a file on the
                                         volume containing a bibliography.
            volumecreationdatetime     - An instance of datetime.datetime giving
                                         the creation date of the volume.
            volumemodificationdatetime - An instance of datetime.datetime giving
                                         the modification date of the volume.
            volumeexpirationdatetime   - An instance of datetime.datetime giving
                                         the expiration date of the volume.
            volumeeffectivedatetime    - An instance of datetime.datetimg giving
                                         the earliest date for which the volume
                                         is valid.
            filestructureversion       - An integer giving the version of the
                                         file structure within the volume. This
                                         value is always 1 for this
                                         implementation.
            applicationdata            - A string of up to 512 bytes containing
                                         arbitrary application data.
            directorytree              - An instance of DirectoryTree describing
                                         the directory hierarchy of this volume.
   """
   @staticmethod
   def DateTimeFromStr(data):
      """Converts an ISO9660 volume descriptor date and time stamp into a
         datetime.datetime object.
            Parameters:
               * data - A string of length 17 containing the date and time
                        information.
            Returns: A datetime.datetime object.
      """
      year = int(data[:4])
      month = int(data[4:6])
      day = int(data[6:8])
      hour = int(data[8:10])
      minute = int(data[10:12])
      second = int(data[12:14])
      microsecond = int(data[14:16]) * 10000
      # signed char in 15-minute increments from GMT.
      offsetminutes = struct.unpack("b", data[16])[0] * 15
      offset = datetime.timedelta(minutes = offsetminutes)
      tzinfo = IsoTzInfo(offset)
      if (year or month or day or hour or minute or second or microsecond
          or offset):
         return datetime.datetime(year, month, day, hour, minute, second,
                                  microsecond, tzinfo)
      return None

   @staticmethod
   def DateTimeToStr(dt):
      """The inverse of the DateTimeFromStr method. Converts a datetime.datetime
         object into a string suitable for recording in date and time fields of
         a volume descriptor.
            Parameters:
               * dt - A datetime.datetime object.
            Retuns: A string of length 17.
      """
      offset = 0
      utcoffset = dt.utcoffset()
      if utcoffset is not None:
         offset = utcoffset.seconds // 900
      centiseconds = dt.microsecond // 10000
      return b"".join((str(dt.year).encode().rjust(4, b"0"),
                      str(dt.month).encode().rjust(2, b"0"),
                      str(dt.day).encode().rjust(2, b"0"),
                      str(dt.hour).encode().rjust(2, b"0"),
                      str(dt.minute).encode().rjust(2, b"0"),
                      str(dt.second).encode().rjust(2, b"0"),
                      str(centiseconds).encode().rjust(2, b"0"),
                      struct.pack(b"b", offset)))

   def __init__(self):
      VolumeDescriptor.__init__(self)
      self.type = 1
      self.version = 1
      self.systemid = b""
      self.volumeid = b""
      self.volumespacesize = 0
      self.volumesetsize = 1
      self.volumesequencenumber = 1
      self.blocksize = SECTOR_SIZE
      self.pathtablesize = 0
      self.lpathtablelocation = 0
      self.loptionalpathtablelocation = 0
      self.mpathtablelocation = 0
      self.moptionalpathtablelocation = 0
      self.rootdirectory = DirectoryRecord.NewDirectory(b"\x00")
      self.volumesetid = b""
      self.publisherid = b""
      self.datapreparerid = b""
      self.applicationid = b""
      self.copyrightfileid = b""
      self.abstractfileid = b""
      self.bibliofileid = b""
      now = datetime.datetime.utcnow()
      self.volumecreationdatetime = now
      self.volumemodificationdatetime = now
      self.volumeexpirationdatetime = now
      self.volumeeffectivedatetime = now
      self.filestructureversion = 1
      self.applicationdata = b""
      self.directorytree = DirectoryTree(self.rootdirectory)

   def _FromStr(self, data):
      self.version = ord(data[6])
      if self.version != 1:
         msg = ("Unknown Descriptor Version for Primary Volume Descriptor: "
                "'%s'." % self.version)
         raise Iso9660Error(msg)
      self.systemid = data[8:40].rstrip()
      self.volumeid = data[40:72].rstrip()
      # 8 bytes reserved.
      self.volumespacesize = GetBothEndian32(data[80:88])
      self.volumesetsize = GetBothEndian16(data[120:124])
      self.volumesequencenumber = GetBothEndian16(data[124:128])
      self.blocksize = GetBothEndian16(data[128:132])
      self.pathtablesize = GetBothEndian32(data[132:140])
      self.lpathtablelocation, self.loptionalpathtablelocation = \
         struct.unpack("<II", data[140:148])
      self.mpathtablelocation, self.moptionalpathtablelocation = \
         struct.unpack(">II", data[148:156])
      self.rootdirectory = DirectoryRecord.FromStr(data[156:190])
      self.directorytree = DirectoryTree(self.rootdirectory)
      self.volumesetid = data[190:318].rstrip()
      self.publisherid = data[318:446].rstrip()
      self.datapreparerid = data[446:574].rstrip()
      self.applicationid = data[574:702].rstrip()
      self.copyrightfileid = data[702:739].rstrip()
      self.abstractfileid = data[739:776].rstrip()
      self.bibliofileid = data[776:813].rstrip()
      try:
         self.volumecreationdatetime = self.DateTimeFromStr(data[813:830])
      except Exception:
         raise Iso9660Error("Invalid volume creation date/time.")
      try:
         self.volumemodificationdatetime = self.DateTimeFromStr(data[830:847])
      except Exception:
         raise Iso9660Error("Invalid volume modification date/time.")
      try:
         self.volumeexpirationdatetime = self.DateTimeFromStr(data[847:864])
      except Exception:
         raise Iso9660Error("Invalid volume expiration date/time.")
      try:
         self.volumeeffectivedatetime = self.DateTimeFromStr(data[864:881])
      except Exception:
         raise Iso9660Error("Invalid volume effective date/time.")
      self.filestructureversion = ord(data[881])
      self.applicationdata = data[883:1395]

   def _ToStr(self):
      return b"".join((VolumeDescriptor._ToStr(self),
                      struct.pack("B", self.version),
                      b"\x00",
                      self.systemid[:32].upper().ljust(32, b"\x20"),
                      self.volumeid[:32].upper().encode().ljust(32, b"\x20"),
                      b"\x00" * 8,
                      MkBothEndian32(self.volumespacesize),
                      b"\x00" * 32,
                      MkBothEndian16(self.volumesetsize),
                      MkBothEndian16(self.volumesequencenumber),
                      MkBothEndian16(self.blocksize),
                      MkBothEndian32(self.pathtablesize),
                      struct.pack("<II", self.lpathtablelocation,
                                         self.loptionalpathtablelocation),
                      struct.pack(">II", self.mpathtablelocation,
                                         self.moptionalpathtablelocation),
                      self.rootdirectory.ToStr(),
                      self.volumesetid[:128].upper().ljust(128, b"\x20"),
                      self.publisherid[:128].upper().ljust(128, b"\x20"),
                      self.datapreparerid[:128].upper().ljust(128, b"\x20"),
                      self.applicationid[:128].upper().encode().ljust(128, b"\x20"),
                      self.copyrightfileid[:37].upper().ljust(37, b"\x20"),
                      self.abstractfileid[:37].upper().ljust(37, b"\x20"),
                      self.bibliofileid[:37].upper().ljust(37, b"\x20"),
                      self.DateTimeToStr(self.volumecreationdatetime),
                      self.DateTimeToStr(self.volumemodificationdatetime),
                      self.DateTimeToStr(self.volumeexpirationdatetime),
                      self.DateTimeToStr(self.volumeeffectivedatetime),
                      struct.pack("B", self.filestructureversion),
                      b"\x00",
                      self.applicationdata[:512].encode().ljust(512, b"\x20"),
                      b"\x00" * 653))

   def LoadDirectoryTree(self, f):
      """Loads the volume's directory hierarchy from a file(-like) object.
            Parameters:
               * f - A file or file-like object from which to read the volume
                     information.
      """
      self.directorytree.LoadTree(f)

   def Finalize(self, offset):
      """Finalizes directory tree and volume fields in preparation for writing
         an ISO9660 file system. Calculates the location of path tables, and
         all directory and file data on the volume. Must be called again if any
         files are added, removed or changed.
            Parameters:
               * offset - The offset in bytes of the first byte after the
                          terminating volume descriptor. I.e., the first byte of
                          variable-length data.
            Returns: The offset at the end of the volume, after the last sector
                     containing file data.
      """
      # Re-check all directory record length attributes. Not sure this is the
      # right place for this, but it becomes important when we calculate path
      # table size later in this method.
      self.directorytree.SetDirectoryLengths()

      # The first thing we will record is the lpathtable.
      self.lpathtablelocation = offset // SECTOR_SIZE

      # Size of the path table, but don't forget that we will write it twice,
      # once for the lpathtable and again for the mpathtable.
      pathtblsz = self.directorytree.GetPathTableSize()
      self.pathtablesize = pathtblsz

      # Advance the 'offset' to the end of the logical block.
      offset += RoundUp(pathtblsz, SECTOR_SIZE)

      # Set location of mpathtable, then again advance offset to the logical
      # block boundary.
      self.mpathtablelocation = offset // SECTOR_SIZE
      offset += RoundUp(pathtblsz, SECTOR_SIZE)

      # Finalize the directory tree. This is where all the magic happens.
      offset = self.directorytree.Finalize(offset)

      # Finally, set volumespacesize to wherever the end of the volume will be.
      self.volumespacesize = offset // SECTOR_SIZE

      return offset

class SupplementaryVolumeDescriptor(PrimaryVolumeDescriptor):
   """A class representing a supplementary volume descriptor. Except for the
      volume descriptor type number, the content is exactly the same as a
      primary volume descriptor.
   """
   def __init__(self):
      VolumeDescriptor.__init__(self)
      self.type = 2

class VolumePartitionDescriptor(VolumeDescriptor):
   """A class representing a volume partition descriptor. Not currently used."""
   def __init__(self):
      VolumeDescriptor.__init__(self)
      self.type = 3
      self.version = 1
      self.systemid = b""
      self.volumepartitionid = b""
      self.volumepartitionlocation = 0
      self.volumepartitionsize = 0
      self.systemdata = b""

   def _FromStr(self, data):
      self.version = ord(data[6])
      if self.version != 1:
         msg = ("Unknown Descriptor Version for Volume Partition Descriptor: "
                "'%s'." % self.version)
         raise Iso9660Error(msg)
      self.systemid = data[8:40].rstrip()
      self.volumepartitionid = data[40:72].rstrip()
      self.volumepartitionlocation = GetBothEndian32(data[72:80])
      self.volumepartitionsize = GetBothEndian32(data[80:88])
      self.systemdata = data[88:SECTOR_SIZE]

   def _ToStr(self, data):
      return b"".join((VolumeDescriptor._ToStr(self),
                      struct.pack("B", self.version),
                      b"\x00",
                      self.systemid[:32].upper().ljust(32, b"\x20"),
                      self.volumepartitionid[:32].upper().ljust(32, b"\x20"),
                      MkBothEndian32(self.volumepartitionlocation),
                      MkBothEndian32(self.volumepartitionsize),
                      self.systemdata[:1960].ljust(1960, b"\x00")))

class VolumeDescriptorSetTerminator(VolumeDescriptor):
   """A class representing a volume descriptor set terminator. This is simply
      a sequence of bytes to indicate the last volume descriptor set on the
      ISO9660 file system.
   """
   def __init__(self):
      VolumeDescriptor.__init__(self)
      self.type = 255
      self.version = 1

   def _FromStr(self, data):
      self.version = ord(data[6])
      if self.version != 1:
         msg = ("Unknown Descriptor Version for Volume Descriptor Set "
                "Terminator: '%s'." % self.version)
         raise Iso9660Error(msg)

   def _ToStr(self):
      header = VolumeDescriptor._ToStr(self) + struct.pack("B", self.version)
      return header + b"\x00" * (SECTOR_SIZE - len(header))

VolumeDescriptor.descriptor_types[0] = BootRecord
VolumeDescriptor.descriptor_types[1] = PrimaryVolumeDescriptor
VolumeDescriptor.descriptor_types[2] = SupplementaryVolumeDescriptor
VolumeDescriptor.descriptor_types[3] = VolumePartitionDescriptor
VolumeDescriptor.descriptor_types[255] = VolumeDescriptorSetTerminator

class FileData(object):
   """A simple class implementing a file-like object to read a selected portion
      of data within a file."""
   def __init__(self, fobj=None, offset=0, length=0):
      """Class constructor.
            Parameters:
               * fobj   - A file object supporting reads. Must also support the
                          seek() method.
               * offset - The offset of fobj where the data to be available in
                          the FileData object begins.
               * length - The length of data to make available in this object.
      """
      self.fobj = fobj
      self.offset = offset
      self.length = length
      self.position = 0

   def Read(self, size=-1):
      if size < 0:
         size = self.length
      self.fobj.seek(self.offset + self.position, 0)
      bytesleft = self.length - self.position
      data = self.fobj.read(min(bytesleft, size))
      self.position += len(data)
      return data
   read = Read

   def Tell(self):
      return self.position
   tell = Tell

   def Seek(self, offset, whence=0):
      if whence == 0:
         newposition = offset
      elif whence == 1:
         newposition = self.position + offset
      elif whence == 2:
         # offset should be negative.
         newposition = self.length + offset
      else:
         raise IOError(22, "Invalid argument")

      if newposition < 0:
         raise IOError(22, "Invalid argument")

      self.position = min(newposition, self.length)
   seek = Seek

class DirectoryRecord(object):
   """A class representing an ISO9660 directory record.
         Attributes:
            directoryrecordlength         - An integer giving the length (in
                                            bytes) of this directory record.
            extendedattributerecordlength - An integer giving the length of any
                                            extended attribute record associated
                                            with this directory record.
            extentlocation                - An integer giving the logical block
                                            in which the directory record data
                                            is recorded.
            datalength                    - The length of the data associated
                                            with this record.
            recordingdatetime             - A datetime.datetime object giving
                                            the date when the directory record
                                            is created.
            fileflags                     - An integer representing the record's
                                            flags. See the "Is" methods of this
                                            class.
            fileunitsize                  - In interleaved mode, represents the
                                            size of each file unit.
            interleavegapsize             - In interleaved mode, represents the
                                            size of the gap between each file
                                            unit.
            volumesequencenumber          - The volume sequence number of the
                                            volume containing the directory
                                            record's associated data.
            fileidlength                  - The length of the fileid.
            filename                      - The file name.
            fileextension                 - The file extension.
            fileversion                   - The file version (as a string).
            systemdata                    - A string containing arbitrary data
                                            to be included in the directory
                                            record.
            fileobj                       - A fileobj associated with the
                                            directory record. Should always be
                                            readable. May represent directory
                                            in an existing directory record
                                            parsed from an existing ISO9660 file
                                            system, or may be a file object to
                                            read data from in order to populate
                                            a new ISO9660 file system.
   """
   FLAG_EXISTENCE = 0x01
   FLAG_DIRECTORY = 0x02
   FLAG_ASSOCIATEDFILE = 0x04
   FLAG_RECORD = 0x08
   FLAG_PROTECTION = 0x10
   FLAG_MULTIEXTENT = 0x80

   def IsHidden(self):
      "True if the record's hidden flag is set."
      return bool(self.fileflags & self.FLAG_EXISTENCE)

   def IsDirectory(self):
      """True if the record's directory flag is set. I.e., the record
         represents a directory, not a file.
      """
      return bool(self.fileflags & self.FLAG_DIRECTORY)

   def IsAssociatedFile(self):
      "True if the record's associated file flag is set."
      return bool(self.fileflags & self.FLAG_ASSOCIATEDFILE)

   def HasRecordFormat(self):
      "True if the record's record flag is set."
      return bool(self.fileflags & self.FLAG_RECORD)

   def IsProtected(self):
      "True if the record's protected flag is set."
      return bool(self.fileflags & self.FLAG_PROTECTION)

   def IsMultiExtent(self):
      "True if the record's multi-extent flag is set."
      return bool(self.fileflags & self.FLAG_MULTIEXTENT)

   @staticmethod
   def DateTimeFromStr(data):
      """Converts the raw data from a directory record's date and time stamp
         into a datetime.datetime object. Note that the date and time format is
         not the same used by the primary volume descriptor.
            Parameters:
               * data - a string of 7 bytes, containing the date/time data from
                        the directory record.
            Returns: A datetime.datetime object.
      """
      (year, month, day, hour, minute, second,
       offsetminutes) = struct.unpack("6Bb", data)
      offsetminutes = offsetminutes * 15
      offset = datetime.timedelta(minutes = offsetminutes)
      tzinfo = IsoTzInfo(offset)
      if year or month or day or hour or minute or second or offset:
         return datetime.datetime(1900 + year, month, day, hour, minute, second,
                                  0, tzinfo)
      return None

   @staticmethod
   def DateTimeToStr(dt):
      """The inverse of DateTimeFromStr.
            Parameters:
               * dt - A datetime.datetime object.
            Returns: A 7-byte string containing the date and time in a format
                     suitable for inserting into a directory record.
      """
      offset = 0
      utcoffset = dt.utcoffset()
      if utcoffset is not None:
         offset = utcoffset.seconds // 900
      return struct.pack("BBBBBBb", dt.year - 1900, dt.month, dt.day, dt.hour,
                                    dt.minute, dt.second, offset)

   def __init__(self):
      self.directoryrecordlength = 0
      self.extendedattributerecordlength = 0
      self.extentlocation = 0
      self.datalength = 0
      self.recordingdatetime = datetime.datetime.utcnow()
      self.fileflags = 0
      self.fileunitsize = 0
      self.interleavegapsize = 0
      self.volumesequencenumber = 1
      self.fileidlength = 0
      self.filename = ""
      self.fileextension = ""
      self.fileversion = ""
      self.systemdata = b""
      self.fileobj = None

   def _setfileid(self, value):
      if self.IsDirectory():
         self.filename = value
         self.fileextension = ""
         self.fileversion = ""
         return

      try:
         nameext, ver = value.split(";", 1)
      except Exception:
         nameext = value
         ver = "1"
      try:
         name, ext = nameext.split(".", 1)
      except Exception:
         name = nameext
         ext = ""
      self.filename = name
      self.fileextension = ext
      self.fileversion = ver

   def _getfileid(self):
      if self.IsDirectory():
         return self.filename
      return "%s.%s;%s" % (self.filename, self.fileextension,
                           self.fileversion)

   fileid = property(_getfileid, _setfileid)

   def __cmp__(self, other):
      compare = lambda x, y: (x > y) - (x < y)

      # This can be used to sort directory records per the standard.
      sz = max(len(self.filename), len(other.filename))
      rc = compare(self.filename.ljust(sz), other.filename.ljust(sz))
      if rc:
         return rc
      sz = max(len(self.fileextension), len(other.fileextension))
      rc = compare(self.fileextension.ljust(sz), other.fileextension.ljust(sz))
      if rc:
         return rc
      rc = -compare(self.fileversion.rjust(6, "\x30"),
                    other.fileversion.rjust(6, "\x30"))
      if rc:
         return rc
      rc = -compare(self.IsAssociatedFile(), other.IsAssociatedFile())
      if rc:
         return rc
      return compare(self.extentlocation, other.extentlocation)

   __lt__ = lambda self, other: self.__cmp__(other) < 0
   __le__ = lambda self, other: self.__cmp__(other) <= 0
   __eq__ = lambda self, other: self.__cmp__(other) == 0
   __ne__ = lambda self, other: self.__cmp__(other) != 0
   __ge__ = lambda self, other: self.__cmp__(other) >= 0
   __gt__ = lambda self, other: self.__cmp__(other) > 0

   @classmethod
   def FromStr(cls, data):
      """Creates a new DirectoryRecord from string data.
            Parameters:
               * data - string data from which to parse the directory record.
               * fobj - An optional file object to associate with the record.
            Returns: A new DirectoryRecord object.
      """
      new = cls()
      new.directoryrecordlength = ord(data[0])
      new.extendedattributerecordlength = ord(data[1])
      new.extentlocation = GetBothEndian32(data[2:10])
      new.datalength = GetBothEndian32(data[10:18])
      try:
         new.recordingdatetime = cls.DateTimeFromStr(data[18:25])
      except Exception:
         raise Iso9660Error("Invalid file recording date/time.")
      new.fileflags = ord(data[25])
      new.fileunitsize = ord(data[26])
      new.interleavegapsize = ord(data[27])
      new.volumesequencenumber = GetBothEndian16(data[28:32])
      new.fileidlength = ord(data[32])
      end = 33 + new.fileidlength
      new.fileid = data[33:end]
      if new.fileidlength % 2 == 0:
         end += 1
      new.systemdata = data[end:new.directoryrecordlength]
      return new

   def Finalize(self):
      "Re-calculates the directoryrecordlength field."
      # Re-calculate fileidlength and directorylength, just to be sure. It's
      # possible someone modified fileid or added system data since the last
      # calculation.
      self.fileidlength = len(self.fileid)
      # If fileid is too long, truncate it.
      if self.fileidlength > 221:
         if self.IsDirectory():
            self.fileid = self.fileid[:221]
         else:
            # We'll lose the extension and the version will revert to 1, but at
            # least it won't corrupt the directory record.
            self.fileid = self.fileid[:217]
         self.fileidlength = 221
      # if fileidlength is even, add a pad byte.
      padlen = (self.fileidlength % 2 == 0) and 1 or 0
      # systemdata cannot overflow the logical sector boundary.
      maxsysdatalen = 2015 - self.fileidlength - padlen
      self.systemdata = self.systemdata[:maxsysdatalen]
      # if sysdata is odd, add a pad byte.
      if len(self.systemdata) % 2:
         self.systemdata += "\x00"
      self.directoryrecordlength = (33 + self.fileidlength + padlen
                                    + len(self.systemdata))

   def ToStr(self):
      """Serializes the DirectoryRecord object to the format found within an
         ISO9660 volume.
      """
      pad = self.fileidlength % 2 == 0 and b"\x00" or b""

      fileid = self.fileid.upper()
      if isinstance(fileid, str):
         fileid = fileid.encode()

      return b"".join((struct.pack("BB", self.directoryrecordlength,
                                        self.extendedattributerecordlength),

                      MkBothEndian32(self.extentlocation),
                      MkBothEndian32(self.datalength),
                      self.DateTimeToStr(self.recordingdatetime),
                      struct.pack("BBB", self.fileflags,
                                         self.fileunitsize,
                                         self.interleavegapsize),
                      MkBothEndian16(self.volumesequencenumber),
                      struct.pack("B", self.fileidlength),
                      fileid,
                      pad,
                      self.systemdata))

   @classmethod
   def FromFile(cls, f):
      "Like FromStr, but parses data from a file object."
      data = f.read(1)
      data += f.read(ord(data) - 1)

      return cls.FromStr(data)

   @classmethod
   def NewDirectory(cls, name):
      """A factory method for creating a new record representing a directory.
            Parameters:
               * name - The directory ID.
            Returns: A new DirectoryRecord object.
      """
      new = cls()
      new.fileid = name
      new.fileidlength = len(name)
      new.fileflags |= cls.FLAG_DIRECTORY
      padlen = new.fileidlength % 2 == 0 and 1 or 0
      new.directoryrecordlength = 33 + new.fileidlength + padlen
      return new

   @classmethod
   def NewFile(cls, fileid, fobj=None, length=0):
      """A factory method for creating a new record representing a file.
            Parameters:
               * fileid - The file ID (in name.extension;version format).
               * fobj   - An optional file object to associate with the record.
               * length - A length argument for the file object. Used when
                          writing an ISO9660 file system in order to allot
                          space in the volume for this record's file data.
      """
      new = cls()
      new.fileid = fileid
      new.fileidlength = len(fileid)
      padlen = (new.fileidlength % 2 == 0) and 1 or 0
      new.directoryrecordlength = 33 + new.fileidlength + padlen
      new.fileobj = fobj
      new.datalength = length
      return new

   def Copy(self):
      """Copy this object to a new object.
            Returns: A new instance of DirectoryRecord.
      """
      new = self.__class__()
      new.directoryrecordlength = self.directoryrecordlength
      new.extendedattributerecordlength = self.extendedattributerecordlength
      new.extentlocation = self.extentlocation
      new.datalength = self.datalength
      new.recordingdatetime = self.recordingdatetime
      new.fileflags = self.fileflags
      new.fileunitsize = self.fileunitsize
      new.interleavegapsize = self.interleavegapsize
      new.volumesequencenumber = self.volumesequencenumber
      new.fileidlength = self.fileidlength
      new.filename = self.filename
      new.fileextension = self.fileextension
      new.fileversion = self.fileversion
      new.systemdata = self.systemdata
      new.fileobj = self.fileobj
      return new
   copy = Copy

class DirectoryTree(object):
   """A class representing the directory hierarchy on an ISO9660 volume.
         Attributes:
            root           - The DirectoryRecord specifying the root of this
                             tree. This may be the root directory for the
                             entire volume, or it may correspond to an entry in
                             a parent directory.
            current        - A DirectoryRecord object corresponding to the
                             current directory. The object is identical to the
                             root attribute, except that that fileid is "\x00".
            parent         - A DirectoryRecord object corresponding to the
                             parent directory. The object is identical to the
                             parent's DirectoryRecord object, except that the
                             fileid is "\x01".
            files          - A list of DirectoryRecord objects representing
                             files in this directory.
            subdirectories - A list of DirectoryTree objects representing
                             sub-directories.
            records        - A list of all records for this directory. Includes
                             references to the objects referenced by current,
                             parent and files, as well as those referenced by
                             the root attribute of each DirectoryTree object in
                             subdirectories.
   """
   def __init__(self, root, parent = None):
      """Class constructor.
            Parameters:
               * root   - A DirectoryRecord object corresponding with this
                          directory's entry in the parent directory.
               * parent - A DirectoryRecord object corresponding with this
                          directory's parent directory.
      """
      # Note: For a sub-directory, 'root' should always be a shared reference
      # to an item in the 'records' list of its parent DirectoryTree object.
      self.root = root
      self.current = root.Copy()
      self.current.filename = "\x00"
      self.current.fileidlength = 1
      if parent is None:
         parent = root
      self.parent = parent.Copy()
      self.parent.filename = "\x01"
      self.parent.fileidlength = 1
      self.files = list()
      self.subdirectories = list()
      self.records = [self.current, self.parent]

   def __cmp__(self, other):
      # Useful for sorting subdirectories, i.e. so that they are filed in
      # proper order in the path tables.
      compare = lambda x, y: (x > y) - (x < y)
      return compare(self.root, other.root)

   __lt__ = lambda self, other: self.__cmp__(other) < 0
   __le__ = lambda self, other: self.__cmp__(other) <= 0
   __eq__ = lambda self, other: self.__cmp__(other) == 0
   __ne__ = lambda self, other: self.__cmp__(other) != 0
   __ge__ = lambda self, other: self.__cmp__(other) >= 0
   __gt__ = lambda self, other: self.__cmp__(other) > 0

   def Sort(self):
      """Sorts all records in this tree in the order specified by the ISO9660
         standard.
      """
      self.records.sort()
      self.files.sort()
      self.subdirectories.sort()
      for child in self.subdirectories:
         child.Sort()

   def LoadTree(self, f):
      """Recursively populates directory tree information from a file object.
            Parameters:
               * f - A file(-like) object from which to read directory
                     records.
      """
      if self.root.interleavegapsize:
         msg = "Cannot load directory tree: Interleaving not supported."
         raise Iso9660Error(msg)

      # Caution: mkisofs sets self.root.datalength to a factor of SECTOR_SIZE,
      # but not all authoring tools do.
      sectors = RoundUp(self.root.datalength, SECTOR_SIZE) / SECTOR_SIZE
      for sector in range(sectors):
         f.seek((self.root.extentlocation + sector) * SECTOR_SIZE, 0)
         data = f.read(1)
         length = ord(data)
         while length:
            data += f.read(length - 1)
            record = DirectoryRecord.FromStr(data)
            self.records.append(record)
            if record.IsDirectory():
               if record.fileid == "\x00":
                  self.records.remove(self.current)
                  self.current = record
               elif record.fileid == "\x01":
                  self.records.remove(self.parent)
                  self.parent = record
               else:
                  self.subdirectories.append(DirectoryTree(record, self.root))
            else:
               record.fileobj = FileData(f, record.extentlocation * SECTOR_SIZE,
                                         record.datalength)
               self.files.append(record)
            data = f.read(1)
            length = ord(data)

      for child in self.subdirectories:
         child.LoadTree(f)

   def AddDirectory(self, item):
      """Inserts a DirectoryRecord representing a directory into this
         DirectoryTree.
            Parameters:
               * item - An instance of either a DirectoryRecord or
                        DirectoryTree. If an instance of DirectoryRecord, a new
                        DirectoryTree object will automatically be created and
                        added to this object's subdirectories and records
                        attributes.
      """
      if isinstance(item, DirectoryRecord):
         newtree = DirectoryTree(item, self.root)
      else:
         newtree = item
      self.records.append(newtree.root)
      self.subdirectories.append(newtree)

   def AddFile(self, record):
      "Inserts a DirectoryRecord representing a file into this DirectoryTree."
      self.records.append(record)
      self.files.append(record)

   def GetPathTableSize(self):
      """Calculates the size of the path table, given the current directories
         in the tree.
      """
      idlen = len(self.root.fileid)
      sz = 8 + idlen + idlen % 2
      for tree in self.subdirectories:
         sz += tree.GetPathTableSize()
      return sz

   def SetDirectoryLengths(self):
      """Calculates the size of each directory, and sets the appropriate values
         in each DirectoryRecord object's datalength attribute. This method
         must be called before the Finalize() method.
      """
      # This makes sure that directories are in proper order. It matters in some
      # corner cases for determining if/where a directory is going to cross
      # sector boundaries.
      self.Sort()
      # This makes sure that the fileidlength is correct for every record.
      for record in self.records:
         record.Finalize()
      datalen = 0
      # Directory records cannot cross sector boundaries.
      for record in self.records:
         sectorboundary = RoundUp(datalen, SECTOR_SIZE)
         if datalen + record.directoryrecordlength >= sectorboundary:
            datalen = sectorboundary + record.directoryrecordlength
         else:
            datalen += record.directoryrecordlength
      # Finally, note that the datalength attribute for a directory's
      # DirectoryRecord object is always a multiple of the logical block size.
      datalen = RoundUp(datalen, SECTOR_SIZE)
      self.root.datalength = datalen
      self.current.datalength = datalen
      for child in self.subdirectories:
         child.parent.datalength = datalen
         child.SetDirectoryLengths()

   def _SetDirectoryLocations(self, offset):
      # Based on offset, determine where all of the DirectoryRecords for
      # directories will go when written to a new file system.
      extentlocation = offset // SECTOR_SIZE
      self.root.extentlocation = extentlocation
      self.current.extentlocation = extentlocation
      # If root directory, set parent location to the the same as current.
      if self.root.filename == "\x00":
         self.parent.extentlocation = extentlocation
      # We are assuming this has already been properly set in
      # _SetDirectoryLengths, i.e., it is a multiple of SECTOR_SIZE.
      offset += self.current.datalength
      for child in self.subdirectories:
         child.parent.extentlocation = extentlocation
         offset = child._SetDirectoryLocations(offset)
      return offset

   def _SetFileLocations(self, offset):
      # Now determine where the data for all of the files will go.
      for record in self.files:
         record.extentlocation = offset // SECTOR_SIZE
         offset += RoundUp(record.datalength, SECTOR_SIZE)
      for child in self.subdirectories:
         offset = child._SetFileLocations(offset)
      return offset

   def Finalize(self, offset):
      """Finalize the location of all directory and file data on the volume.
            Parameters:
               * offset - The offset where the root directory's data will be
                          written. This is normally the first sector after the
                          path tables.
            Returns: The offset at the end of all data. This can be used to
                     determine how large the volume will be when written.
      """
      offset = self._SetDirectoryLocations(offset)
      # Set file data locations.
      offset = self._SetFileLocations(offset)
      return offset

   def _WritePathTables(self, f):
      # Tricky stuff. Not only does the specification say we need to write
      # directories in breadth-first order, but each directory entry contains
      # a reference to the index in the table for its parent directory. The way
      # we do this is to iterate the 'current' list while populating the 'next'
      # list with all of the 'current' members' children. When we reach the end
      # of 'current', 'next' becomes 'current', and we start the loop again. We
      # finish when there are no more children. Given that directories are
      # already sorted within their own ranks, this produces the desired path
      # table ordering.
      current = [(self, 1)] # start at the root directory, index 1.
      dirlist = list()
      while current:
         next = list()
         for tree, parentindex in current:
            # ISO9660's path table numbers indexes starting at 1.
            currentindex = len(dirlist) + 1
            dirlist.append((tree.root.fileidlength,
                            tree.root.extendedattributerecordlength,
                            tree.root.extentlocation, parentindex,
                            tree.root.fileid))
            for child in tree.subdirectories:
               next.append((child, currentindex))
         current = next

      lvals = list()
      mvals = list()
      for idlen, earlen, loc, parent, dirid in dirlist:
         # Add padding if necessary, to make an even number of bytes.
         if isinstance(dirid, str):
            dirid = dirid.encode()
         dirid = dirid.upper() + b"\x00" * (idlen % 2)
         lvals.append(struct.pack("<BBIH", idlen, earlen, loc, parent) + dirid)
         mvals.append(struct.pack(">BBIH", idlen, earlen, loc, parent) + dirid)
      ltable = b"".join(lvals)
      mtable = b"".join(mvals)
      tablelen = RoundUp(len(ltable), SECTOR_SIZE)
      f.write(ltable.ljust(tablelen, b"\x00"))
      f.write(mtable.ljust(tablelen, b"\x00"))
      return tablelen * 2

   def _WriteDirectoryRecords(self, f):
      datalen = 0
      # self.records is already sorted, and contains everything we need to
      # write out for this particular directory. Same algorithm used in
      # _SetDirectoryLengths to figure out when we need to advance to a new
      # sector boundary.
      for record in self.records:
         sectorboundary = RoundUp(datalen, SECTOR_SIZE)
         if datalen + record.directoryrecordlength >= sectorboundary:
            f.write(b"\x00" * (sectorboundary - datalen))
            f.write(record.ToStr())
            datalen = sectorboundary + record.directoryrecordlength
         else:
            f.write(record.ToStr())
            datalen += record.directoryrecordlength
      f.write(b"\x00" * (RoundUp(datalen, SECTOR_SIZE) - datalen))

      for child in self.subdirectories:
         datalen += child._WriteDirectoryRecords(f)

      return datalen

   def _WriteFileData(self, f):
      datalen = 0
      for record in self.files:
         filedata = record.fileobj.read(WRITE_READ_CHUNK_SIZE)
         filelen = len(filedata)
         while filedata:
            f.write(filedata)
            filedata = record.fileobj.read(WRITE_READ_CHUNK_SIZE)
            filelen += len(filedata)
         assert(filelen == record.datalength)
         filelen = RoundUp(filelen, SECTOR_SIZE)
         f.write(b"\x00" * (filelen - record.datalength))
         datalen += filelen

      for child in self.subdirectories:
         datalen += child._WriteFileData(f)

      return datalen

   def Write(self, f):
      """Write the path tables, directory records and file data in the
         DirectoryTree to a file descriptor.
            Parameters:
               * f - must support a write() method.
            Returns: The length of data written.
      """
      datalen = 0
      datalen += self._WritePathTables(f)
      datalen += self._WriteDirectoryRecords(f)
      datalen += self._WriteFileData(f)
      return datalen

   def ExtractFiles(self, path):
      """Extract all files in the directory hierarchy.
            Parameters:
               * path - A directory to extract the ISO9660 volume's contents to.
                        The directory will be created if it does not exist. Any
                        existing files with the same name as files on the
                        volume will be overwritten.
      """
      if self.root.fileid == "\x00":
         outdir = path
      else:
         outdir = os.path.join(path, self.root.fileid)

      if not os.path.exists(outdir):
         os.makedirs(outdir)

      for record in self.files:
         fn = ".".join((record.filename, record.fileextension))
         fout = open(os.path.join(outdir, fn), "wb")
         fin = record.fileobj
         data = fin.read(SECTOR_SIZE)
         while data:
            fout.write(data)
            data = fin.read(SECTOR_SIZE)
         fout.close()

      for child in self.subdirectories:
         child.ExtractFiles(outdir)

class ElToritoValidationEntry(object):
   """Represents a validation entry in an ElTorito boot catalog.
         Attributes:
            * headerid   - An int. Must always be 1.
            * platformid - An int. Valid values are 0 for PC, 1 for PowerPC,
                           2 for Mac, and 0xEF for UEFI.
            * idstring   - A string of 24 bytes or less. Intended to identify
                           the volume's developer/manufacturer.
            * checksum   - A string representing a 16-bit checksum. Should not
                           be modified directly.
            * key        - Must always be "\x55\xAA".
   """
   def __init__(self):
      self.headerid = 1
      self.platformid = 0
      self.idstring = b""
      self.checksum = 0
      self.key = b"\x55\xAA"

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string of 32 bytes.
            Returns: A new ElToritoValidationEntry object.
      """
      new = cls()
      (new.headerid, new.platformid, new.idstring,
       new.checksum, new.key) = struct.unpack("<2B2x24sH2s", data)
      checksum = new.CheckSum()
      if new.checksum != checksum:
         msg = ("Invalid checksum. (Calculated 0x%04X, found 0x%04X.)" %
                (checksum, new.checksum))
         raise ElToritoError(msg)
      return new

   def CheckSum(self):
      """Calculates a checksum based on the object data.
            Returns: An integer (the checksum value).
      """
      # The checksum is a 16-bit word such that summing all 16 16-bit words in
      # the record comes out to 0 (assuming result is also wrapped to a 16-bit
      # word).

      packedWord = struct.pack("<BB", self.headerid, self.platformid)
      bytesum = struct.unpack("<H", packedWord)[0] + \
                struct.unpack("<H", self.key)[0]

      idstring = self.idstring[:24].ljust(24, b"\x00")
      for i in range(0, 24, 2):
         bytesum += struct.unpack("<H", idstring[i:i+2])[0]
      # Only do the math if bytesum is non-zero.
      checksum = bytesum and 0x10000 - (bytesum & 0xFFFF)
      return checksum

   def ToStr(self):
      """Serializes the object into the byte representation used in an
         ElTorito boot catalog.
            Returns: A string of 32 bytes.
      """
      self.checksum = self.CheckSum()
      idstring = self.idstring[:24].ljust(24, b"\x00")
      return struct.pack("<2B2x24sH2s", self.headerid, self.platformid,
                         idstring, self.checksum, self.key)

class ElToritoSectionHeader(object):
   """Represents an optional section header in an ElTorito boot catalog.
         Attributes:
            * hascontinuation - True if there are additional catalog sections
                                following this one.
            * platformid      - An int. Valid values are 0 for PC, 1 for
                                PowerPC, 2 for Mac and 0xEF for UEFI.
            * sectionentries  - The number of ElToritoSectionEntry instances.
            * idstring        - A string of 28 bytes to be matched by the BIOS
                                software.
   """
   def __init__(self):
      self.hascontinuation = False
      self.platformid = 0
      self.sectionentries = 0
      self.idstring = b""

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string of 32 bytes.
            Returns: A new ElToritoSectionHeader object.
      """
      new = cls()
      (indicator, new.platformid, new.sectionentries,
       new.idstring) = struct.unpack("<BBH28s")
      new.hascontinuation = (indicator == 0x90)
      return new

   def ToStr(self):
      """Serialize the object to the format recorded in a boot catalog.
            Returns: A string of 32 bytes.
      """
      indicator = self.hascontinuation and 0x90 or 0x91
      return struct.pack("<BBH28s", indicator, self.platformid,
                         self.sectionentries,
                         self.idstring[:28].ljust(28, b"\x00"))

class ElToritoSectionEntryExtension(object):
   """Represents an optional extension to a boot catalog entry.
         Attributes:
            * hascontinuation   - True if additional extension entries follow
                                  this one.
            * selectioncriteria - A string of 30 bytes, matched by the BIOS
                                  software.
   """
   def __init__(self):
      self.hascontinuation = False
      self.selectioncriteria = ""

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string of 32 bytes.
            Returns: A new ElToritoSectionEntryExtension object.
      """
      new = cls()
      bootmediatype, new.selectioncriteria = struct.unpack("xB30s")
      new.hascontinuation = bootmediatype & 0x10
      return new

   def ToStr(self, data):
      """Serialize the object to the format recorded in a boot catalog.
            Returns: A string of 32 bytes.
      """
      bootmediatype = self.hascontinuation and 0x10 or 0
      return struct.pack("BB30s", "\x44", bootmediatype,
                         self.selectioncriteria[:30].ljust(30, "\x00"))

class ElToritoSectionEntry(object):
   """Represents an ElTorito section in a boot catalog.
         Attributes:
            * bootindicator         - An integer. 0x88 if the entry is bootable,
                                      or 0 if it is not. (Defaults to 0x88.)
            * loadsegment           - An integer representing the load segment
                                      for the boot image. Defaults to 0.
            * systemtype            - The type of the system. This must match
                                      the system type from the partition table
                                      found in the boot image.
            * sectorcount           - The number of virtual sectors to load
                                      from the boot image.
            * loadrba               - The logical block of the ISO volume where
                                      the boot image starts.
            * selectioncriteriatype - Currently 1 or 0. 1 indicates language
                                      and version selection criteria. 0
                                      indicates no selection criteria. For the
                                      default/initial section entry, this must
                                      be 0 (which is the default value).
            * selectioncriteria     - A string of length 19, specifying the
                                      selection criteria to be matched by the
                                      BIOS.
            * continuations         - A list of ElToritoSectionEntryExtension
                                      objects.
         Properties:
            * mediatype             - Must be one of the constants
                                      NO_EMULATION (the default),
                                      EMULATE_12FLOPPY, EMULATE_144FLOPPY,
                                      EMULATE_288FLOPPY, or EMULATE_HDD.
                                      Indicates the emulation the BIOS must
                                      provide for the boot image.
            * hascontinuation       - A boolean indicating whether one or more
                                      extensions follow the entry.
            * hasatapidriver        - A boolean indicating whether the boot
                                      image provides an ATAPI driver.
            * hasscsidriver         - A boolean indicating whether the boot
                                      image provides a SCSI driver.
   """
   NO_EMULATION = 0
   EMULATE_12FLOPPY = 1
   EMULATE_144FLOPPY = 2
   EMULATE_288FLOPPY = 3
   EMULATE_HDD = 4
   HAS_CONTINUATION = 0x10
   HAS_ATAPI_DRIVER = 0x20
   HAS_SCSI_DRIVER = 0x40

   _EMULATION_TYPES = (NO_EMULATION, EMULATE_12FLOPPY, EMULATE_144FLOPPY,
                       EMULATE_288FLOPPY, EMULATE_HDD)

   def __init__(self):
      self.bootindicator = 0x88
      self._bootmediatype = 0
      self.loadsegment = 0
      self.systemtype = 0
      self.sectorcount = 0
      self.loadrba = 0
      # Initial/Default entry should set these two values to 0 and "".
      self.selectioncriteriatype = 0
      self.selectioncriteria = ""
      self.continuations = list()

   def __len__(self):
      return (len(self.continuations) + 1) * 32

   def _GetSetMediaType(self, *args):
      if args:
         if args[0] not in self._EMULATION_TYPES:
            raise ValueError("Invalid emulation media type '%s'." % args[0])
         # Clear bits 0-3, saving bits 4-7, then turn on any bits in value.
         self._bootmediatype = self._bootmediatype & 0xF0 | args[0]
      else:
         return self._bootmediatype & 0xF

   def _GetSetHasContinuation(self, *args):
      if args:
         if args[0]:
            self._bootmediatype |= self.HAS_CONTINUATION
         else:
            self._bootmediatype &= ~self.HAS_CONTINUATION
      else:
         return bool(self._bootmediatype & self.HAS_CONTINUATION)

   def _GetSetHasAtapiDriver(self, *args):
      if args:
         if args[0]:
            self._bootmediatype |= self.HAS_ATAPI_DRIVER
         else:
            self._bootmediatype &= ~self.HAS_ATAPI_DRIVER
      else:
         return bool(self._bootmediatype & self.HAS_ATAPI_DRIVER)

   def _GetSetHasScsiDriver(self, *args):
      if args:
         if args[0]:
            self._bootmediatype |= self.HAS_SCSI_DRIVER
         else:
            self._bootmediatype &= ~self.HAS_SCSI_DRIVER
      else:
         return bool(self._bootmediatype & self.HAS_SCSI_DRIVER)

   mediatype = property(_GetSetMediaType, _GetSetMediaType)
   hascontinuation = property(_GetSetHasContinuation, _GetSetHasContinuation)
   hasatapidriver = property(_GetSetHasAtapiDriver, _GetSetHasAtapiDriver)
   hasscsidriver = property(_GetSetHasScsiDriver, _GetSetHasScsiDriver)

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string of 32 or more bytes. The string may include
                        additional section extensions.
            Returns: A new ElToritoSectionEntry object.
      """
      new = cls()
      (new.bootindicator, new._bootmediatype, new.loadsegment, new.systemtype,
       new.sectorcount, new.loadrba, new.selectioncriteriatype,
       new.selectioncriteria) = struct.unpack("<BBHBxHIB19s", data[:32])
      if not new.hascontinuation or len(data) < 64:
         return new
      data = data[32:]
      while len(data) >= 32:
         continuation = ElToritoSectionEntryExtension.FromStr(data[:32])
         new.continuations.append(continuation)
         if not continuation.hascontinuation:
            break
         data = data[32:]
      return new

   def ToStr(self):
      """Serialize the object to the format recorded in a boot catalog.
            Returns: A string containing the section entry and any entry
                     extensions.
      """
      if self.continuations:
         self.hascontinuation = True
         for extension in self.continuations[:-1]:
            extension.hascontinuation = True
         self.continuations[-1].hascontinuation = False

      selectioncriteria = self.selectioncriteria[:19].ljust(19, "\x00")
      data = struct.pack("<BBHBxHIB19s", self.bootindicator,
                         self._bootmediatype, self.loadsegment, self.systemtype,
                         self.sectorcount, self.loadrba,
                         self.selectioncriteriatype, selectioncriteria.encode())
      values = [data]
      for continuation in self.continuations:
         values.append(continuation.ToStr())
      return b"".join(values)

class ElToritoSection(object):
   """Represents a boot catalog section.
         Attributes:
            * sectionheader - An instance of ElToritoSectionHeader.
            * sections      - A list of ElToritoSectionEntry objects.
   """
   def __init__(self):
      self.sectionheader = ElToritoSectionHeader()
      self.sections = list()

   def __len__(self):
      return sum(len(x) for x in self.sections) + 32

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string of 32 or more bytes. The string should include
                        the section header, sections and any section extensions.
            Returns: A new ElToritoSection object.
      """
      new = cls()
      if not len(data) >= 32 or ord(data[0]) not in (0x90, 0x91):
         raise ElToritoError("File object does not contain a section header.")
      new.sectionheader = ElToritoSectionHeader.FromStr(data[:32])
      for n in new.sectionheader.sectionentries:
         data = data[32:]
         if len(data) < 32:
            raise ElToritoError("Unexpected end of section data.")
         entry = ElToritoSectionEntry.FromStr(data[:32])
         new.sections.append(entry)
         if entry.hascontinuation:
            data = data[32:]
            if len(data) < 32:
               raise ElToritoError("Unexpected end of section data.")
            ext = ElToritoSectionEntryExtension.FromStr(data[:32])
            entry.continuations.append(ext)
            while ext.hascontinuation:
               data = data[32:]
               if len(data) < 32:
                  raise ElToritoError("Unexpected end of section data.")
               ext = ElToritoSectionEntryExtension.FromStr(data[32:])
               entry.continuations.append(ext)
      return new

   def ToStr(self):
      """Serialize the object to the format recorded in a boot catalog.
            Returns: A string containing the section header, section entries
                     and any entry extensions.
      """
      self.sectionheader.sectionentries = len(self.sections)
      if self.sections:
         self.sections[-1].hascontinuation = False
         for section in self.sections[:-1]:
            section.hascontinuation = True
      output = [self.sectionheader.ToStr()]
      for section in self.sections:
         output.append(section.ToStr())
      return b"".join(output)

class ElToritoBootCatalog(object):
   """Represents an ElTorito boot catalog.
         Attributes:
            * validationentry - An instance of ElToritoValidationEntry.
            * defaultentry    - An instance of ElToritoSectionEntry.
            * sections        - A list of ElToritoSection objects.
   """
   def __init__(self):
      self.validationentry = ElToritoValidationEntry()
      self.defaultentry = ElToritoSectionEntry()
      self.sections = list()

   def __len__(self):
      # 64 = 32 bytes for validation entry; 32 bytes for default entry.
      datalength = 64 + sum(len(x) for x in self.sections)
      return RoundUp(datalength, 512)

   @classmethod
   def FromStr(cls, data):
      """Create a new object from the format recorded in a boot catalog.
            Parameters:
               * data - A string containing the boot catalog data. The string
                        should include the validation entry and default entry,
                        and the section headers and sections for any additional
                        boot images.
            Returns: A new ElToritoBootCatalog object.
      """
      new = cls()
      if len(data) < 64:
         raise ElToritoError("Unexpected end of boot catalog.")
      new.validationentry = ElToritoValidationEntry.FromStr(data[:32])
      new.defaultentry = ElToritoSectionEntry.FromStr(data[32:64])
      data = data[64:]
      if len(data) >= 32 and ord(data[0]) in (0x90, 0x91):
         section = ElToritoSection.FromStr(data)
         new.sections.append(section)
         while section.sectionheader.hascontinuation:
            # Each record is 32 bytes, and there is a record for each section
            # entry + the section header itself.
            data = data[(section.sectionheader.sectionentries + 1) * 32:]
            if not data:
               raise ElToritoError("Unexpected end of boot section entries.")
            section = ElToritoSection.FromStr(data)
            new.sections.append(section)
      return new

   def ToStr(self):
      """Serialize the object to the format recorded in a boot catalog.
            Returns: A string containing the entire boot catalog in a format
                     appropriate for inserting into an ISO image. The data is
                     padded to the nearest (512-byte) virtual sector boundary.
      """
      output = [self.validationentry.ToStr(),self.defaultentry.ToStr()]
      if self.sections:
         self.sections[-1].sectionheader.hascontinuation = False
         for section in self.sections[:-1]:
            section.sectionheader.hascontinuation = True
      for section in self.sections:
         output.append(section.ToStr())
      # Pad output to next "virtual sector" boundary.
      outputlen = sum(len(x) for x in output)
      output.append(b"\x00" * (RoundUp(outputlen, 512) - outputlen))
      return b"".join(output)

class BootImage(object):
   """Represents a boot image to be added to the ISO9660 volume.
         Attributes:
            * record             - An instance of DirectoryRecord, specifying
                                   the boot image. The object must have a
                                   valid fileobj attribute.
            * checksum           - A 32-bit checksum calculated over the boot
                                   image, starting at offset 64. This should
                                   not be directly modified, and will be
                                   automatically calculated if needed. It is
                                   only used when embedbootinfotable is True.
            * embedbootinfotable - If True, a boot info table will be embedded
                                   in the boot image at offsets 8-64.
            * loadsectors        - The number of virtual (512-byte) sectors of
                                   the image that should be loaded.
            * emulationtype      - The type of emulation to use, one of
                                   ElToritoSectionEntry.NO_EMULATION,
                                   ElToritoSectionEntry.EMULATE_12FLOPPY,
                                   ElToritoSectionEntry.EMULATE_144FLOPPY,
                                   ElToritoSectionEntry.EMULATE_288FLOPPY,
                                   or ElToritoSectionEntry.EMULATE_HDD.
            * platformid         - 0 for PC, 1 for PowerPC, 2 for Mac or
                                   0xEF for UEFI.
   """
   def __init__(self, record, embedbootinfotable=True, emulationtype=0,
                loadsectors=4, platformid=0):
      if record.fileobj is None:
         raise ElToritoError("DirectoryRecord object for boot image must "
                             "have a valid file object.")
      self.record = record
      self.checksum = 0
      self.embedbootinfotable = embedbootinfotable
      self.loadsectors = loadsectors
      self.emulationtype = emulationtype
      self.platformid = platformid
      # Copy the source of the boot image to a temp location and replace
      # record.fileobj if we need to embed the bootinfotable. Might as well
      # calculate the checksum while we're at it.
      if embedbootinfotable:
         tmpf = tempfile.TemporaryFile()
         data = record.fileobj.read(64)
         if len(data) < 64:
            raise ElToritoError("Boot image is too short.")

         # checksum starts at offset 64:
         checksum = 0
         while data:
            tmpf.write(data)
            data = record.fileobj.read(4)
            checksum += struct.unpack("<I", data.ljust(4, b"\x00"))[0]
         tmpf.seek(0, 0)
         record.fileobj = tmpf
         self.checksum = checksum & 0xFFFFFFFF

   def EmbedBootInfoTable(self, pvdlocation=16):
      """Embeds a boot info table into the boot image binary.
            Parameters:
               * pvdlocation - The location of primary volume descriptor. This
                               is almost universally 16.
            Note: This method must be called once the Iso9660Volume object is
                  finalized, but before writing. It relies on knowing the
                  offset of the boot image file.
      """
      if not self.embedbootinfotable:
         return
      infotable = struct.pack("<IIII40x", pvdlocation,
                              self.record.extentlocation,
                              self.record.datalength,
                              self.checksum)
      self.record.fileobj.seek(8, 0)
      self.record.fileobj.write(infotable)
      self.record.fileobj.seek(0, 0)

class Iso9660Volume(object):
   """A class representing an ISO9660 file system.
         Attributes:
            systemdata                     - Either None, or a file-like object
                                             from which system data can be read.
                                             If not None, data will be read from
                                             the file object to populate the
                                             first 32768 bytes of the file
                                             system (the "system data area").
            bootrecord                     - Either None, or an instance of
                                             BootRecord. If an instance of
                                             BootRecord, a boot record will be
                                             written to the file system.
            primaryvolumedescriptor        - An instance of
                                             PrimaryVolumeDescriptor.
            supplementaryvolumedescriptors - A list of instances of
                                             SupplementaryVolumeDescriptor. Not
                                             currently supported.
            volumepartitiondescriptors     - A list of instances of
                                             VolumePartitionDescriptor. Not
                                             currently supported.
   """

   DEFAULT_BOOT_CAT = "BOOT.CAT;1"

   def __init__(self):
      self.systemdata = None
      self.bootrecord = None
      self.primaryvolumedescriptor = PrimaryVolumeDescriptor()
      self.supplementaryvolumedescriptors = list()
      self.volumepartitiondescriptors = list()
      self.bootimage = None
      self.altbootimages = list()
      self.bootcatalog = None
      self._bootcatrecord = None

   @classmethod
   def FromFile(cls, f):
      """Populates an Iso9660Volume object from a file object.
            Parameters:
               * f - A file-like object supporting read().
            Returns: A new Iso9660Volume object.
      """
      if isString(f):
         f = open(f, "rb")
      new = cls()
      new.systemdata = FileData(f, f.tell(), SECTOR_SIZE * 16)
      f.seek(SECTOR_SIZE * 16, 1)
      volumedescriptor = VolumeDescriptor.FromFile(f)
      while volumedescriptor.type != 255:
         if volumedescriptor.type == 0:
            new.bootrecord = volumedescriptor
         elif volumedescriptor.type == 1:
            new.primaryvolumedescriptor = volumedescriptor
         elif volumedescriptor.type == 2:
            new.supplementaryvolumedescriptors.append(volumedescriptor)
         elif volumedescriptor.type == 3:
            new.volumepartitiondescriptors.append(volumedescriptor)
         volumedescriptor = VolumeDescriptor.FromFile(f)
      new.primaryvolumedescriptor.LoadDirectoryTree(f)
      for svd in new.supplementaryvolumedescriptors:
         svd.LoadDirectoryTree(f)

      return new

   def AddFile(self, f, isopath="", length=None):
      """Add a file to the ISO9660 volume.
            Parameters:
               * f       - A file name or a file object.
               * isopath - The name of the file on the ISO9660 file system.
                           Must be a valid file name. If f is a file name and
                           isopath is not specified, it defaults to the base-
                           name of the file path (i.e. it is added to the root
                           of the ISO). If f is a file object, then isopath
                           must be specified, or the file object must support a
                           valid 'name' attribute.
            Returns: The added DirectoryRecord.
            Raises:
               * IOError    - f is not a valid file object or file name, or
                              cannot be read.
               * ValueError - isopath or length is invalid.
      """
      if isString(f):
         fobj = open(f, "rb")
         if not isopath:
            isopath = os.path.basename(f)
      else:
         fobj = f
         if not isopath:
            if hasattr(f, "name"):
               isopath = os.path.basename(f.name)
            else:
               raise ValueError("Must specify isopath parameter.")

      if length is None:
         try:
            s = os.fstat(fobj.fileno())
            length = s.st_size
         except OSError:
            raise ValueError("Must specify length parameter.")

      # Split on either forward slash or backslash. Directory names can be up
      # to 31 characters long.
      pathparts = [x[:31].upper() for y in isopath.split("\\")
                   for x in y.split("/")]
      dirnames = pathparts[:-1]
      fn = pathparts[-1]
      if len(dirnames) > MAX_DIRECTORY_DEPTH:
         raise ValueError("isopath specifies too many levels of directories.")

      # Drill down through the directory levels to either find existing
      # directories, or create them as we go.
      lasttree = self.primaryvolumedescriptor.directorytree
      for dirname in dirnames:
         found = None
         for subtree in lasttree.subdirectories:
            if subtree.root.fileid == dirname:
               found = subtree
               break
         if found is not None:
            lasttree = found
         else:
            newtree = DirectoryTree(DirectoryRecord.NewDirectory(dirname))
            lasttree.AddDirectory(newtree)
            lasttree = newtree

      try:
         name, ext = fn.split(".")
      except Exception:
         name = fn
         ext = ""

      # If we already have a file in this directory with the same name and
      # extension, remove it and replace with the new file. Note that we go
      # backwards, so as not to shift array indexes during the loop.
      for i in range(len(lasttree.records)-1, -1, -1):
         record = lasttree.records[i]
         if (record.filename.upper() == name.upper() and
             record.fileextension.upper() == ext.upper()):
            # Remove record from "special" boot files, if applicable:
            if self.bootimage is not None and self.bootimage.record == record:
               self.bootimage = None
            if (self._bootcatrecord is not None
                and record == self._bootcatrecord):
               self._bootcatrecord = None
            for j in range(len(self.altbootimages)-1, -1, -1):
               if record == self.altbootimages[j].record:
                  del self.altbootimages[j]
            # Remove record from "files" list.
            for j in range(len(lasttree.files)-1, -1, -1):
               if record == lasttree.files[j]:
                  del lasttree.files[j]
            del lasttree.records[i]

      fileid = "%s.%s;1" % (name, ext)
      record = DirectoryRecord.NewFile(fileid, fobj, length)
      lasttree.AddFile(record)
      return record

   def GetFile(self, fn):
      """Retrieve a file from the ISO9660 volume.
            Parameters:
               * fn      - The path of the file, relative to the volume root.
            Returns: The DirectoryRecord associated with the file name.
            Raises:
               * KeyError   - The file name does not exist.
               * ValueError - The specified path is not valid.
      """
      fn = fn.strip("\\/")
      pathparts = [x.upper() for y in fn.split("\\") for x in y.split("/") if x]
      if not pathparts:
         raise ValueError("Not a valid path '%s'." % fn)
      dirs = pathparts[:-1]
      try:
         filename, fileext = pathparts[-1].split(".")
      except Exception:
         filename = pathparts[-1]
         fileext = ""
      lasttree = self.primaryvolumedescriptor.directorytree
      while dirs:
         dirname = dirs.pop(0)
         for tree in lasttree.subdirectories:
            if tree.root.fileid == dirname:
               lasttree = tree
               break
      if dirs:
         raise KeyError(fn)
      for record in lasttree.files:
         if record.filename == filename and record.fileextension == fileext:
            return record
      raise KeyError(fn)

   def _MkElToritoBootRecord(self):
      # Creates an ElTorito boot record on the volume, if one does not yet
      # exist.
      self.bootrecord = BootRecord()
      self.bootrecord.bootsystem = b"EL TORITO SPECIFICATION".ljust(32, b"\x00")
      self.bootrecord.bootid = b"\x00" * 32

   def _MkElToritoBootCatalog(self):
      if not self.bootcatalog:
         self.bootcatalog = ElToritoBootCatalog()

      # Add relevant sections and headers, so that length of boot catalog can
      # be calculated correctly.
      del self.bootcatalog.sections[:]
      for image in self.altbootimages:
         found = False
         for section in self.bootcatalog.sections:
            if section.platformid == image.platformid:
               found = True
               break
         if not found:
            section = ElToritoSection()
            section.platformid = image.platformid
            self.bootcatalog.sections.append(section)
         section.sections.append(ElToritoSectionEntry())

      if self._bootcatrecord is None:
         record = DirectoryRecord()
         record.fileid = self.DEFAULT_BOOT_CAT
         self.primaryvolumedescriptor.directorytree.AddFile(record)
         self._bootcatrecord = record

      self._bootcatrecord.datalength = len(self.bootcatalog)

   def SetBootImage(self, record, embedbootinfotable=True, emulationtype=0,
                    loadsectors=4, platformid=0):
      """Sets a record as the boot image for the volume.
            Paramters:
               * record             - An instance of DirectoryRecord,
                                      specifying the boot image.
               * embedbootinfotable - If True, a boot info table will be
                                      embedded into the boot image. This is
                                      required for isolinux, and defaults to
                                      True.
               * emulationtype      - The appropriate emulation type for the
                                      boot image. Should be one of
                                      ElToritoSectionEntry.NO_EMULATION,
                                      ElToritoSectionEntry.EMULATE_12FLOPPY,
                                      ElToritoSectionEntry.EMULATE_144FLOPPY,
                                      ElToritoSectionEntry.EMULATE_288FLOPPY,
                                      or ElToritoSectionEntry.EMULATE_HDD.
               * loadsectors        - The number of sectors to load from the
                                      boot image. Defaults to 4.
               * platformid         - The appropriate platform ID. 0 for
                                      PC, 1 for PowerPC, 2 for Mac, 0xEF for
                                      UEFI. Defaults to 0.
            Note: The record must be a record already added to the primary
                  volume.
      """
      if self.bootimage is not None:
         self.altbootimages.append(self.bootimage)
      self.bootimage = BootImage(record, embedbootinfotable, emulationtype,
                                 loadsectors, platformid)

   def AddAltBootImage(self, record, embedbootinfotable=False, emulationtype=0,
                       loadsectors=1, platformid=0xEF):
      """Adds an alternate boot image for the volume.
            Paramters:
               * record             - An instance of DirectoryRecord,
                                      specifying the boot image.
               * embedbootinfotable - If True, a boot info table will be
                                      embedded into the boot image. Defaults to
                                      False.
               * emulationtype      - The appropriate emulation type for the
                                      boot image. Should be one of
                                      ElToritoSectionEntry.NO_EMULATION,
                                      ElToritoSectionEntry.EMULATE_12FLOPPY,
                                      ElToritoSectionEntry.EMULATE_144FLOPPY,
                                      ElToritoSectionEntry.EMULATE_288FLOPPY,
                                      or ElToritoSectionEntry.EMULATE_HDD.
               * loadsectors        - The number of sectors to load from the
                                      boot image. Defaults to 1.
               * platformid         - The appropriate platform ID. 0 for
                                      PC, 1 for PowerPC, 2 for Mac, 0xEF for
                                      UEFI. Defaults to 0xEF.
            Note: The record must be a record already added to the primary
                  volume.
      """
      bootimage = BootImage(record, embedbootinfotable, emulationtype,
                            loadsectors, platformid)
      if self.bootimage is None:
         self.bootimage = bootimage
      else:
         self.altbootimages.append(bootimage)

   def _FinalizeElTorito(self):
      # Must be called after finalizing Primary Volume Descriptor, as it needs
      # to know offsets of the boot image and boot catalog.
      if None in (self.bootimage, self.bootrecord, self.bootcatalog):
         return

      self.bootimage.EmbedBootInfoTable()
      defaultentry = self.bootcatalog.defaultentry
      defaultentry.mediatype = self.bootimage.emulationtype
      defaultentry.sectorcount = self.bootimage.loadsectors
      defaultentry.loadrba = self.bootimage.record.extentlocation
      self.bootcatalog.validationentry.platformid = self.bootimage.platformid

      del self.bootcatalog.sections[:]
      for image in self.altbootimages:
         image.EmbedBootInfoTable()
         found = False
         for section in self.bootcatalog.sections:
            if section.sectionheader.platformid == image.platformid:
               found = True
               break
         if not found:
            section = ElToritoSection()
            section.sectionheader.platformid = image.platformid
            self.bootcatalog.sections.append(section)
         newentry = ElToritoSectionEntry()
         newentry.mediatype = image.emulationtype
         newentry.sectorcount = image.loadsectors
         newentry.loadrba = image.record.extentlocation
         section.sections.append(newentry)

      self._bootcatrecord.fileobj = BytesIO(self.bootcatalog.ToStr())
      self.bootrecord.bootdata = struct.pack("<I1970x",
                                             self._bootcatrecord.extentlocation)

   def Finalize(self):
      """Finalize the size and location of data structures in the ISO9660
         volume in preparation for writing. Removing, adding or changing any
         files or directories requires re-running this method before calling
         Write().
            Returns: The volume length. May be used to determine the amount of
                     space required to write out the new file system.
      """
      # 16 sectors system use area + 1 sector for PVD + 1 sector for terminator
      # = 18 sectors.
      offset = 18 * SECTOR_SIZE

      if self.bootimage is not None:
         self._MkElToritoBootRecord()
         self._MkElToritoBootCatalog()

      if self.bootrecord is not None:
         offset += SECTOR_SIZE

      # Figure out how much space we need for the rest of it.
      offset = self.primaryvolumedescriptor.Finalize(offset)
      self._FinalizeElTorito()
      return offset

   def Write(self, f):
      """Write out an ISO9660 file system. Must call Finalize() before calling
         this method.
            Parameters:
               * f - a file-like object supporting the write() method.
            Returns: The number of bytes written.
      """
      if isString(f):
         fobj = open(f, "wb")
      else:
         fobj = f

      # systemdata if you have it; otherwise 16 empty sectors.
      datalen = 0
      if self.systemdata:
         data = self.systemdata.read(SECTOR_SIZE)
         while data and datalen <= 32768:
            fobj.write(data)
            datalen += len(data)
      fobj.write(b"\x00" * (32768 - datalen))
      datalen = 32768

      data = self.primaryvolumedescriptor.ToStr()
      fobj.write(data)
      datalen += len(data)

      if self.bootrecord:
         data = self.bootrecord.ToStr()
         fobj.write(data)
         datalen += len(data)

      data = VolumeDescriptorSetTerminator().ToStr()
      fobj.write(data)
      datalen += len(data)

      datalen += self.primaryvolumedescriptor.directorytree.Write(fobj)
      return datalen

   def ExtractFiles(self, path):
      """Extract all files from the volume.
            Parameters:
               * path - A directory to which to extract the volume contents.
                        Will be created if it does not exist. Any existing
                        files in the directory with the same name as those on
                        the ISO9660 volume will be overwritten.
      """
      self.primaryvolumedescriptor.directorytree.ExtractFiles(path)

def Main(op, isoname, dirname, bootimage=None, efibootimage=None):

   if op == "extract":
      if not os.path.exists(dirname):
         os.makedirs(dirname)
      volume = Iso9660Volume.FromFile(isoname)
      volume.ExtractFiles(dirname)

   elif op == "create":
      volume = Iso9660Volume()
      pvd = volume.primaryvolumedescriptor
      pvd.systemid = b"Test System"
      pvd.volumeid = b"Test Volume"
      pvd.applicationid = b"Test Application"

      dirnamelen = len(dirname) + 1
      for dirpath, dirnames, filenames in os.walk(dirname, topdown=True):
         for filename in filenames:
            fn = os.path.join(dirpath, filename)
            volume.AddFile(fn, fn[dirnamelen:])
      if bootimage and efibootimage:
         record = volume.AddFile(bootimage, os.path.basename(bootimage))
         volume.SetBootImage(record)
         record = volume.AddFile(efibootimage, os.path.basename(efibootimage))
         volume.AddAltBootImage(record)
      elif bootimage:
         record = volume.AddFile(bootimage, os.path.basename(bootimage))
         volume.SetBootImage(record)
      elif efibootimage:
         record = volume.AddFile(efibootimage, os.path.basename(efibootimage))
         volume.SetBootImage(record, embedbootinfotable=False, loadsectors=1,
                             platformid=0xEF)
      volume.Finalize()
      volume.Write(isoname)

   else:
      raise ValueError("Invalid operation '%s'." % op)

if __name__ == "__main__":
   import getopt

   def usage():
      subs = {"progname": os.path.basename(sys.argv[0])}
      sys.stderr.write("Usage:\n"
                       "   %(progname)s -c output.iso [-b bootimage] "
                       "[-e efibootimage] directory\n"
                       "   %(progname)s -x input.iso directory\n"
                       % subs)
      sys.exit(1)

   try:
      opts, args = getopt.getopt(sys.argv[1:], "c:b:e:x:")
   except getopt.GetoptError as err:
      sys.stderr.write("%s\n" % err)
      usage()

   op = ""
   bootimage = None
   efibootimage = None
   for o, a in opts:
      if o == "-c":
         op = "create"
         isoname = a
      elif o == "-b":
         bootimage = a
      elif o == "-e":
         efibootimage = a
      elif o == "-x":
         op = "extract"
         isoname = a
      else:
         sys.stderr.write("Unknown option '%s'\n" % o)
         usage()

   if not op:
      sys.stderr.write("Must specify -c or -x.\n")
      usage()

   if len(args) < 1:
      sys.stderr.write("Must specify directory.\n")
      usage()
   dirname = os.path.abspath(args[0])

   Main(op, isoname, dirname, bootimage, efibootimage)
