[Scummvm-git-logs] scummvm master -> 8ace566818d2afe966e2a849fda1962500d43c37
sev-
noreply at scummvm.org
Sat Feb 7 22:31:14 UTC 2026
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
8ace566818 DEVTOOLS: PYCDLIB: Removed accidentally added files
Commit: 8ace566818d2afe966e2a849fda1962500d43c37
https://github.com/scummvm/scummvm/commit/8ace566818d2afe966e2a849fda1962500d43c37
Author: Eugene Sandulenko (sev at scummvm.org)
Date: 2026-02-07T23:31:01+01:00
Commit Message:
DEVTOOLS: PYCDLIB: Removed accidentally added files
Changed paths:
A devtools/contrib/pycdlib/pycdlib/pycdlib.py
R devtools/contrib/pycdlib/pycdlib/pycdlib.py.orig
R idevtools/contrib/pycdlib/pycdlib/pycdlib.py
diff --git a/idevtools/contrib/pycdlib/pycdlib/pycdlib.py b/devtools/contrib/pycdlib/pycdlib/pycdlib.py
similarity index 100%
rename from idevtools/contrib/pycdlib/pycdlib/pycdlib.py
rename to devtools/contrib/pycdlib/pycdlib/pycdlib.py
diff --git a/devtools/contrib/pycdlib/pycdlib/pycdlib.py.orig b/devtools/contrib/pycdlib/pycdlib/pycdlib.py.orig
deleted file mode 100644
index dbe68a1518c..00000000000
--- a/devtools/contrib/pycdlib/pycdlib/pycdlib.py.orig
+++ /dev/null
@@ -1,6203 +0,0 @@
-# Copyright (C) 2015-2022 Chris Lalancette <clalancette at gmail.com>
-
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation;
-# version 2.1 of the License.
-
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-
-"""Main PyCdlib class and support classes and utilities."""
-
-import bisect
-import collections
-import functools
-import inspect
-import io
-import os
-import struct
-import sys
-import time
-
-from pycdlib import dr
-from pycdlib import eltorito
-from pycdlib import facade
-from pycdlib import headervd
-from pycdlib import inode
-from pycdlib import isohybrid
-from pycdlib import path_table_record
-from pycdlib import pycdlibexception
-from pycdlib import pycdlibio
-from pycdlib import udf as udfmod
-from pycdlib import utils
-
-# For mypy annotations
-if False: # pylint: disable=using-constant-test
- from typing import Any, BinaryIO, Callable, Deque, Dict, Generator, IO, List, Optional, Tuple, Union # NOQA pylint: disable=unused-import
-
-# There are a number of specific ways that numerical data is stored in the
-# ISO9660/Ecma-119 standard. In the text these are reference by the section
-# number they are stored in. A brief synopsis:
-#
-# 7.1.1 - 8-bit number
-# 7.2.3 - 16-bit number, stored first as little-endian then as big-endian (4 bytes total)
-# 7.3.1 - 32-bit number, stored as little-endian
-# 7.3.2 - 32-bit number ,stored as big-endian
-# 7.3.3 - 32-bit number, stored first as little-endian then as big-endian (8 bytes total)
-
-# We allow A-Z, 0-9, and _ as "d1" characters. The below is the fastest way to
-# build that list as integers.
-_allowed_d1_characters = set(tuple(range(65, 91)) + tuple(range(48, 58)) + tuple((ord(b'_'),)))
-
-
-def _check_d1_characters(name):
- # type: (bytes) -> None
- """
- A function to check that a name only uses d1 characters as defined by ISO9660.
-
- Parameters:
- name - The name to check.
- Returns:
- Nothing.
- """
- for char in bytearray(name):
- if char not in _allowed_d1_characters:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 filenames must consist of characters A-Z, 0-9, and _')
-
-
-def _split_iso9660_filename(fullname):
- # type: (bytes) -> Tuple[bytes, bytes, bytes]
- """
- A function to split an ISO 9660 filename into its constituent parts. This
- is the name, the extension, and the version number.
-
- Parameters:
- fullname - The name to split.
- Returns:
- A tuple containing the name, extension, and version.
- """
- namesplit = fullname.split(b';')
- version = b''
- if len(namesplit) > 1:
- version = namesplit.pop()
-
- rest = b';'.join(namesplit)
-
- dotsplit = rest.split(b'.')
- if len(dotsplit) == 1:
- name = dotsplit[0]
- extension = b''
- else:
- name = b'.'.join(dotsplit[:-1])
- extension = dotsplit[-1]
-
- return (name, extension, version)
-
-
-def _check_iso9660_filename(fullname, interchange_level):
- # type: (bytes, int) -> None
- """
- A function to check that a file identifier conforms to the ISO9660 rules
- for a particular interchange level.
-
- Parameters:
- fullname - The name to check.
- interchange_level - The interchange level to check against.
- Returns:
- Nothing.
- """
-
- # Ensure the filename is valid according to Ecma-119 7.5.
-
- (name, extension, version) = _split_iso9660_filename(fullname)
-
- # Ecma-119 says that filenames must end with a semicolon-number, but we have
- # found ISOs in the wild (Ubuntu 14.04 Desktop i386) that do not follow
- # this. Thus we allow for names both with and without the semi+version.
-
- # Ecma-119 says that filenames must have a version number, but we have
- # found ISOs in the wild (FreeBSD 10.1 amd64) that do not have any version
- # number. Allow for this.
-
- if version != b'' and (int(version) < 1 or int(version) > 32767):
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 filenames must have a version between 1 and 32767')
-
- # Ecma-119 section 7.5.1 specifies that filenames must have at least one
- # character in either the name or the extension.
- if not name and not extension:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 filenames must have a non-empty name or extension')
-
- if b';' in name or b';' in extension:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 filenames must contain exactly one semicolon')
-
- if interchange_level == 1:
- # According to Ecma-119, section 10.1, at level 1 the filename can
- # only be up to 8 d-characters or d1-characters, and the extension can
- # only be up to 3 d-characters or 3 d1-characters.
- if len(name) > 8 or len(extension) > 3:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 filenames at interchange level 1 cannot have more than 8 characters or 3 characters in the extension')
- else:
- # For all other interchange levels, the maximum filename length is
- # specified in Ecma-119 7.5.2. However, we have found ISOs in the wild
- # (Ubuntu 14.04 Desktop i386) that don't conform to this.
- pass
-
- # Ecma-119 section 7.5.1 says that the file name and extension each contain
- # zero or more d-characters or d1-characters. While the definition of
- # d-characters and d1-characters is not specified in Ecma-119,
- # http://wiki.osdev.org/ISO_9660 suggests that this consists of A-Z, 0-9, _
- # which seems to correlate with empirical evidence.
- if interchange_level < 4:
- _check_d1_characters(name)
- _check_d1_characters(extension)
-
-
-def _check_iso9660_directory(fullname, interchange_level):
- # type: (bytes, int) -> None
- """
- A function to check that an directory identifier conforms to the ISO9660
- rules for a particular interchange level.
-
- Parameters:
- fullname - The name to check.
- interchange_level - The interchange level to check against.
- Returns:
- Nothing.
- """
-
- # Ensure the directory name is valid according to Ecma-119 7.6.
-
- # Ecma-119 section 7.6.1 says that a directory identifier needs at least one
- # character
- if not fullname:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 directory names must be at least 1 character long')
-
- maxlen = float('inf')
- if interchange_level == 1:
- # Ecma-119 section 10.1 says that directory identifiers lengths cannot
- # exceed 8 at interchange level 1.
- maxlen = 8
- elif interchange_level in (2, 3):
- # Ecma-119 section 7.6.3 says that directory identifiers lengths cannot
- # exceed 207.
- maxlen = 207
- # for interchange_level 4, we allow any length
-
- if len(fullname) > maxlen:
- raise pycdlibexception.PyCdlibInvalidInput('ISO9660 directory names at interchange level %d cannot exceed %d characters' % (interchange_level, maxlen))
-
- # Ecma-119 section 7.6.1 says that directory names consist of one or more
- # d-characters or d1-characters. While the definition of d-characters and
- # d1-characters is not specified in Ecma-119,
- # http://wiki.osdev.org/ISO_9660 suggests that this consists of A-Z, 0-9, _
- # which seems to correlate with empirical evidence.
- if interchange_level < 4:
- _check_d1_characters(fullname)
-
-
-def _interchange_level_from_filename(fullname):
- # type: (bytes) -> int
- """
- A function to determine the ISO interchange level from the filename.
- In theory, there are 3 levels, but in practice we only deal with level 1
- and level 3.
-
- Parameters:
- fullname - The name to use to determine the interchange level.
- Returns:
- The interchange level determined from this filename.
- """
- (name, extension, version) = _split_iso9660_filename(fullname)
-
- interchange_level = 1
-
- if version != b'' and (int(version) < 1 or int(version) > 32767):
- interchange_level = 3
-
- if b';' in name or b';' in extension:
- interchange_level = 3
-
- if len(name) > 8 or len(extension) > 3:
- interchange_level = 3
-
- try:
- _check_d1_characters(name)
- _check_d1_characters(extension)
- except pycdlibexception.PyCdlibInvalidInput:
- interchange_level = 3
-
- return interchange_level
-
-
-def _interchange_level_from_directory(name):
- # type: (bytes) -> int
- """
- A function to determine the ISO interchange level from the directory name.
- In theory, there are 3 levels, but in practice we only deal with level 1
- and level 3.
-
- Parameters:
- name - The name to use to determine the interchange level.
- Returns:
- The interchange level determined from this filename.
- """
- interchange_level = 1
- if len(name) > 8:
- interchange_level = 3
-
- try:
- _check_d1_characters(name)
- except pycdlibexception.PyCdlibInvalidInput:
- interchange_level = 3
-
- return interchange_level
-
-
-def _reassign_vd_dirrecord_extents(vd, current_extent):
- # type: (headervd.PrimaryOrSupplementaryVD, int) -> Tuple[int, List[inode.Inode]]
- """
- An internal helper method for reassign_extents that assigns extents to
- directory records for the passed in Volume Descriptor. The current
- extent is passed in, and this function returns the extent after the
- last one it assigned.
-
- Parameters:
- vd - The volume descriptor on which to operate.
- current_extent - The current extent before assigning extents to the
- volume descriptor directory records.
- Returns:
- The current extent after assigning extents to the volume descriptor
- directory records.
- """
- log_block_size = vd.logical_block_size()
-
- root_dir_record = vd.root_directory_record()
- root_dir_record.set_data_location(current_extent, 0)
- current_extent += utils.ceiling_div(root_dir_record.data_length,
- log_block_size)
-
- # Walk through the list, assigning extents to all of the directories.
- child_link_recs = []
- parent_link_recs = []
- file_list = []
- ptr_index = 1
- dirs = collections.deque([root_dir_record])
- while dirs:
- dir_record = dirs.popleft()
-
- if dir_record.is_root:
- # The root directory record doesn't need an extent assigned,
- # so just add its children to the list and continue on
- for child in dir_record.children:
- if child.ptr is not None:
- child.ptr.update_parent_directory_number(ptr_index)
- ptr_index += 1
- dirs.extend(dir_record.children)
- continue
-
- dir_record_parent = dir_record.parent
-
- if dir_record_parent is None:
- raise pycdlibexception.PyCdlibInternalError('Parent of record is empty, this should never happen')
-
- if dir_record.is_dot():
- dir_record.set_data_location(dir_record_parent.extent_location(), 0)
- continue
-
- dir_record_rock_ridge = dir_record.rock_ridge
-
- if dir_record.is_dotdot():
- if dir_record_parent.is_root:
- # Special case of the root directory record. In this case, we
- # set the dotdot extent location to the same as the root.
- dir_record.set_data_location(dir_record_parent.extent_location(),
- 0)
- continue
-
- if dir_record_parent.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Grandparent of record is empty, this should never happen')
- dir_record.set_data_location(dir_record_parent.parent.extent_location(),
- 0)
-
- # Now that we've set the data location, move around the Rock Ridge
- # links if necessary.
- if dir_record_rock_ridge is not None:
- if dir_record_rock_ridge.parent_link is not None:
- parent_link_recs.append(dir_record)
-
- if dir_record_parent.rock_ridge is not None:
- if dir_record_parent.parent is not None:
- if dir_record_parent.parent.is_root:
- source_dr = dir_record_parent.parent.children[0]
- else:
- source_dr = dir_record_parent.parent
-
- if source_dr is None or source_dr.rock_ridge is None:
- raise pycdlibexception.PyCdlibInternalError('Expected directory record to have Rock Ridge')
- dir_record_rock_ridge.copy_file_links(source_dr.rock_ridge)
- continue
-
- if dir_record.is_dir():
- dir_record.set_data_location(current_extent, current_extent)
- for child in dir_record.children:
- if child.ptr is not None:
- child.ptr.update_parent_directory_number(ptr_index)
- ptr_index += 1
- if dir_record_rock_ridge is None or not dir_record_rock_ridge.child_link_record_exists():
- current_extent += utils.ceiling_div(dir_record.data_length,
- log_block_size)
- dirs.extend(dir_record.children)
- else:
- if dir_record.data_length == 0 or (dir_record_rock_ridge is not None and (dir_record_rock_ridge.child_link_record_exists() or dir_record_rock_ridge.is_symlink())):
- # If this is a child link record, the extent location really
- # doesn't matter, since it is fake. We set it to zero.
- dir_record.set_data_location(0, 0)
- else:
- if dir_record.inode is not None:
- file_list.append(dir_record.inode)
-
- if dir_record_rock_ridge is not None:
- if dir_record_rock_ridge.dr_entries.ce_record is not None and dir_record_rock_ridge.ce_block is not None:
- if dir_record_rock_ridge.ce_block.extent_location() < 0:
- dir_record_rock_ridge.ce_block.set_extent_location(current_extent)
- current_extent += 1
- dir_record_rock_ridge.dr_entries.ce_record.update_extent(dir_record_rock_ridge.ce_block.extent_location())
- if dir_record_rock_ridge.cl_to_moved_dr is not None:
- child_link_recs.append(dir_record)
-
- # After we have reshuffled the extents, update the rock ridge links.
- for ch in child_link_recs:
- if ch.rock_ridge is not None:
- ch.rock_ridge.child_link_update_from_dirrecord()
-
- for p in parent_link_recs:
- if p.rock_ridge is not None:
- p.rock_ridge.parent_link_update_from_dirrecord()
-
- return current_extent, file_list
-
-
-def _check_path_depth(iso_path):
- # type: (bytes) -> None
- """
- An internal method to take a fully-qualified iso path and check whether
- it meets the path depth requirements of ISO9660/Ecma-119.
-
- Parameters:
- iso_path - The path to check.
- Returns:
- Nothing.
- """
- if len(utils.split_path(iso_path)) > 7:
- # Ecma-119 Section 6.8.2.1 says that the number of levels in the
- # hierarchy shall not exceed eight. Since the root directory must be
- # at level 1 by itself, the effective maximum hierarchy depth is 7.
- raise pycdlibexception.PyCdlibInvalidInput('Directory levels too deep (maximum is 7)')
-
-
-def _yield_children(rec, rr):
- # type: (dr.DirectoryRecord, bool) -> Generator
- """
- An internal function to gather and yield all of the children of a Directory
- Record.
-
- Parameters:
- rec - The Directory Record to get all of the children from (must be a
- directory).
- rr - Whether to follow Rock Ridge relocation entries or not.
- Yields:
- Children of this Directory Record.
- Returns:
- Nothing.
- """
- if not rec.is_dir():
- raise pycdlibexception.PyCdlibInvalidInput('Record is not a directory!')
-
- last = b''
- for child in rec.children:
- # If the filename of this child is the same as the last one, skip the
- # child. This can happen if there is a very large file with more than
- # one directory entry.
- fi = child.file_identifier()
- if fi == last:
- continue
- last = fi
-
- skip_child = False
- if rr:
- if child.rock_ridge is not None:
- for inner_child in child.children:
- if inner_child.is_dotdot():
- if inner_child.rock_ridge is not None and inner_child.rock_ridge.parent_link_record_exists():
- skip_child = True
- break
-
- if skip_child:
- continue
-
- if child.rock_ridge.child_link_record_exists() and \
- child.rock_ridge.cl_to_moved_dr is not None and \
- child.rock_ridge.cl_to_moved_dr.parent is not None:
- # This is a relocated entry. We want to find the entry this
- # was relocated to; we do that by following the child_link,
- # then going up to the parent and finding the entry that
- # links to the same one as this one.
- cl_parent = child.rock_ridge.cl_to_moved_dr.parent
- for cl_child in cl_parent.children:
- if cl_child.rock_ridge is not None and cl_child.rock_ridge.name() == child.rock_ridge.name():
- child = cl_child
- break
- # If we didn't find the relocated entry in the parent of the
- # moved entry, weird; just yield the one we would have anyway.
-
- yield child
-
-
-def _find_dr_record_by_name(vd, path, encoding):
- # type: (headervd.PrimaryOrSupplementaryVD, bytes, str) -> dr.DirectoryRecord
- """
- An internal function to find a directory record on the ISO given an ISO
- or Joliet path. If the entry is found, it returns the directory record
- object corresponding to that entry. If the entry could not be found,
- a pycdlibexception.PyCdlibInvalidInput exception is raised.
-
- Parameters:
- vd - The Volume Descriptor to look in for the Directory Record.
- path - The ISO or Joliet entry to find the Directory Record for.
- encoding - The string encoding used for the path.
- Returns:
- The directory record entry representing the entry on the ISO.
- """
- root_dir_record = vd.root_directory_record()
-
- # If the path is just the slash, we want to return the root directory.
- if path == b'/':
- return root_dir_record
-
- splitpath = utils.split_path(path)
-
- currpath = splitpath.pop(0).decode('utf-8').encode(encoding)
-
- entry = root_dir_record
-
- tmpdr = dr.DirectoryRecord()
-
- while True:
- child = None
-
- thelist = entry.children
- lo = 2
- hi = len(thelist)
- while lo < hi:
- mid = (lo + hi) // 2
- tmpdr.file_ident = currpath
- if thelist[mid] < tmpdr:
- lo = mid + 1
- else:
- hi = mid
- index = lo
- if index != len(thelist) and thelist[index].file_ident == currpath:
- child = thelist[index]
-
- if child is None:
- # We failed to find this component of the path, so break out of the
- # loop and fail.
- break
-
- if child.rock_ridge is not None and child.rock_ridge.child_link_record_exists():
- # The rock ridge extension has a child link, so follow it.
- child = child.rock_ridge.cl_to_moved_dr
- if child is None:
- break
-
- # We found the last child we are looking for; return it.
- if not splitpath:
- return child
-
- if not child.is_dir():
- break
- entry = child
- currpath = splitpath.pop(0).decode('utf-8').encode(encoding)
-
- raise pycdlibexception.PyCdlibInvalidInput('Could not find path')
-
-
-class PyCdlib:
- """The main class for manipulating ISOs."""
- __slots__ = ('_initialized', '_cdfp', 'pvds', 'svds', 'vdsts', 'brs', 'pvd',
- 'rock_ridge', '_always_consistent', '_has_udf', 'joliet_vd',
- 'eltorito_boot_catalog', 'isohybrid_mbr', '_managing_fp', 'xa',
- '_needs_reshuffle', '_rr_moved_record', '_rr_moved_name',
- '_rr_moved_rr_name', 'enhanced_vd', 'version_vd', 'inodes',
- 'interchange_level', '_write_check_list', '_track_writes',
- 'udf_beas', 'udf_nsr', 'udf_teas', 'udf_anchors',
- 'udf_main_descs', 'udf_reserve_descs',
- 'udf_logical_volume_integrity', 'udf_boots',
- 'udf_logical_volume_integrity_terminator', 'udf_root',
- 'udf_file_set', 'udf_file_set_terminator',
- 'logical_block_size')
-
- def _initialize(self):
- # type: () -> None
- """
- An internal method to re-initialize the object. Called from
- both __init__ and close.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- self._cdfp = io.BytesIO() # type: IO[Any]
- self.svds = [] # type: List[headervd.PrimaryOrSupplementaryVD]
- self.brs = [] # type: List[headervd.BootRecord]
- self.vdsts = [] # type: List[headervd.VolumeDescriptorSetTerminator]
- self.eltorito_boot_catalog = None # type: Optional[eltorito.EltoritoBootCatalog]
- self._initialized = False
- self.rock_ridge = ''
- self.isohybrid_mbr = None # type: Optional[isohybrid.IsoHybrid]
- self.xa = False
- self._managing_fp = False
- self.pvds = [] # type: List[headervd.PrimaryOrSupplementaryVD]
- self._has_udf = False
- self.udf_beas = [] # type: List[udfmod.BEAVolumeStructure]
- self.udf_boots = [] # type: List[udfmod.UDFBootDescriptor]
- self.udf_nsr = udfmod.NSRVolumeStructure()
- self.udf_teas = [] # type: List[udfmod.TEAVolumeStructure]
- self.udf_anchors = [] # type: List[udfmod.UDFAnchorVolumeStructure]
- self.udf_main_descs = udfmod.UDFDescriptorSequence()
- self.udf_reserve_descs = udfmod.UDFDescriptorSequence()
- self.udf_logical_volume_integrity = None # type: Optional[udfmod.UDFLogicalVolumeIntegrityDescriptor]
- self.udf_logical_volume_integrity_terminator = None # type: Optional[udfmod.UDFTerminatingDescriptor]
- self.udf_root = None # type: Optional[udfmod.UDFFileEntry]
- self.udf_file_set = udfmod.UDFFileSetDescriptor()
- self.udf_file_set_terminator = None # type: Optional[udfmod.UDFTerminatingDescriptor]
- self._needs_reshuffle = False
- self._rr_moved_record = dr.DirectoryRecord()
- self._rr_moved_name = None # type: Optional[bytes]
- self._rr_moved_rr_name = None # type: Optional[bytes]
- self.enhanced_vd = None # type: Optional[headervd.PrimaryOrSupplementaryVD]
- self.joliet_vd = None # type: Optional[headervd.PrimaryOrSupplementaryVD]
- self._find_iso_record.cache_clear() # pylint: disable=no-member
- self._find_rr_record.cache_clear() # pylint: disable=no-member
- self._find_joliet_record.cache_clear() # pylint: disable=no-member
- self._find_udf_record.cache_clear() # pylint: disable=no-member
- self._write_check_list = [] # type: List[PyCdlib._WriteRange]
- self.version_vd = None # type: Optional[headervd.VersionVolumeDescriptor]
- self.inodes = [] # type: List[inode.Inode]
- # Default to a logical block size of 2048; this will be overridden by
- # the block size from the PVD or the detected block size during an open.
- self.logical_block_size = 2048
- self.interchange_level = 1 # type: int
-
- def _parse_volume_descriptors(self):
- # type: () -> None
- """
- An internal method to parse the volume descriptors on an ISO.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- # Ecma-119, 6.2.1 says that the Volume Space is divided into a System
- # Area and a Data Area, where the System Area is in logical sectors 0
- # to 15, and whose contents is not specified by the standard. Logical
- # sectors are 2048 bytes in length, so we start at offset 16 * 2048.
- self._cdfp.seek(16 * 2048)
- while True:
- # All volume descriptors are exactly 2048 bytes long
- curr_extent = self._cdfp.tell() // 2048
- vd = self._cdfp.read(2048)
- if len(vd) != 2048:
- raise pycdlibexception.PyCdlibInvalidISO('Failed to read entire volume descriptor')
- (desc_type, ident) = struct.unpack_from('=B5s', vd, 0)
- if desc_type not in (headervd.VOLUME_DESCRIPTOR_TYPE_PRIMARY,
- headervd.VOLUME_DESCRIPTOR_TYPE_SET_TERMINATOR,
- headervd.VOLUME_DESCRIPTOR_TYPE_BOOT_RECORD,
- headervd.VOLUME_DESCRIPTOR_TYPE_SUPPLEMENTARY) or ident not in (b'CD001', b'CDW02', b'BEA01', b'NSR02', b'NSR03', b'TEA01', b'BOOT2'):
- # We read the next extent, and it wasn't a descriptor. Abort
- # the loop, remembering to back up the input file descriptor.
- self._cdfp.seek(-2048, os.SEEK_CUR)
- break
- if desc_type == headervd.VOLUME_DESCRIPTOR_TYPE_PRIMARY:
- pvd = headervd.PrimaryOrSupplementaryVD(headervd.VOLUME_DESCRIPTOR_TYPE_PRIMARY)
- pvd.parse(vd, curr_extent)
- self.pvds.append(pvd)
- elif desc_type == headervd.VOLUME_DESCRIPTOR_TYPE_SET_TERMINATOR:
- vdst = headervd.VolumeDescriptorSetTerminator()
- vdst.parse(vd, curr_extent)
- self.vdsts.append(vdst)
- elif desc_type == headervd.VOLUME_DESCRIPTOR_TYPE_BOOT_RECORD:
- # Both an Ecma-119 Boot Record and a Ecma-TR 071 UDF-Bridge
- # Beginning Extended Area Descriptor have the first byte as 0,
- # so we can't tell which it is until we look at the next 5
- # bytes (Boot Record will have 'CD001', BEAD will have 'BEA01').
- if ident == b'CD001':
- br = headervd.BootRecord()
- br.parse(vd, curr_extent)
- self.brs.append(br)
- elif ident == b'BEA01':
- self._has_udf = True
- udf_bea = udfmod.BEAVolumeStructure()
- udf_bea.parse(vd, curr_extent)
- self.udf_beas.append(udf_bea)
- elif ident in (b'NSR02', b'NSR03'):
- self.udf_nsr.parse(vd, curr_extent)
- elif ident == b'TEA01':
- udf_tea = udfmod.TEAVolumeStructure()
- udf_tea.parse(vd, curr_extent)
- self.udf_teas.append(udf_tea)
- elif ident == b'BOOT2':
- udf_boot = udfmod.UDFBootDescriptor()
- udf_boot.parse(vd, curr_extent)
- self.udf_boots.append(udf_boot)
- else:
- # This isn't really possible, since we would have aborted
- # the loop above.
- raise pycdlibexception.PyCdlibInvalidISO('Invalid volume identification type')
- elif desc_type == headervd.VOLUME_DESCRIPTOR_TYPE_SUPPLEMENTARY:
- svd = headervd.PrimaryOrSupplementaryVD(headervd.VOLUME_DESCRIPTOR_TYPE_SUPPLEMENTARY)
- svd.parse(vd, curr_extent)
- self.svds.append(svd)
- # Since we checked for the valid descriptors above, it is impossible
- # to see an invalid desc_type here, so no check necessary.
-
- # The language in Ecma-119, p.8, Section 6.7.1 says:
- #
- # The sequence shall contain one Primary Volume Descriptor (see 8.4)
- # recorded at least once.
- #
- # The important bit there is "at least one", which means that we have
- # to accept ISOs with more than one PVD.
- if not self.pvds:
- raise pycdlibexception.PyCdlibInvalidISO('Valid ISO9660 filesystems must have at least one PVD')
-
- self.pvd = self.pvds[0]
-
- # Make sure any other PVDs agree with the first one.
- for pvd in self.pvds[1:]:
- if pvd != self.pvd:
- raise pycdlibexception.PyCdlibInvalidISO('Multiple occurrences of PVD did not agree!')
-
- pvd.root_dir_record = self.pvd.root_dir_record
-
- if not self.vdsts:
- raise pycdlibexception.PyCdlibInvalidISO('Valid ISO9660 filesystems must have at least one Volume Descriptor Set Terminator')
-
- def _seek_to_extent(self, extent):
- # type: (int) -> None
- """
- An internal method to seek to a particular extent on the input ISO.
-
- Parameters:
- extent - The extent to seek to.
- Returns:
- Nothing.
- """
- self._cdfp.seek(extent * self.logical_block_size)
-
- @functools.lru_cache(maxsize=256)
- def _find_iso_record(self, iso_path, encoding='utf-8'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- An internal method to find a directory record on the ISO given an ISO
- path. If the entry is found, it returns the directory record object
- corresponding to that entry. If the entry could not be found, a
- pycdlibexception.PyCdlibInvalidInput is raised.
-
- Parameters:
- iso_path - The ISO9660 path to lookup.
- encoding - The string encoding used for the path.
- Returns:
- The directory record entry representing the entry on the ISO.
- """
- return _find_dr_record_by_name(self.pvd, iso_path, encoding)
-
- @functools.lru_cache(maxsize=256)
- def _find_rr_record(self, rr_path, encoding='utf-8'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- An internal method to find a directory record on the ISO given a Rock
- Ridge path. If the entry is found, it returns the directory record
- object corresponding to that entry. If the entry could not be found, a
- pycdlibexception.PyCdlibInvalidInput is raised.
-
- Parameters:
- rr_path - The Rock Ridge path to lookup.
- encoding - The string encoding used for the path.
- Returns:
- The directory record entry representing the entry on the ISO.
- """
- root_dir_record = self.pvd.root_directory_record()
-
- # If the path is just the slash, return the root directory.
- if rr_path == b'/':
- return root_dir_record
-
- splitpath = utils.split_path(rr_path)
-
- currpath = splitpath.pop(0).decode('utf-8').encode(encoding)
-
- entry = root_dir_record
-
- while True:
- child = None
-
- thelist = entry.rr_children
- # The list could be empty because we don't store dot or dotdot
- # entries in Rock Ridge. If that is the case, just get out and
- # fail since we definitely didn't find what we were looking for.
- if not thelist:
- break
-
- lo = 0
- hi = len(thelist)
- while lo < hi:
- mid = (lo + hi) // 2
-
- tmpchild = thelist[mid]
-
- if tmpchild.rock_ridge is None:
- raise pycdlibexception.PyCdlibInvalidInput('Record without Rock Ridge entry on Rock Ridge ISO')
-
- if tmpchild.rock_ridge.name() < currpath:
- lo = mid + 1
- else:
- hi = mid
- index = lo
- tmpchild = thelist[index]
- if index != len(thelist) and tmpchild.rock_ridge is not None and tmpchild.rock_ridge.name() == currpath:
- child = thelist[index]
-
- if child is None:
- # We failed to find this component of the path, so break out of
- # the loop and fail
- break
-
- if child.rock_ridge is not None and child.rock_ridge.child_link_record_exists():
- # The rock ridge extension has a child link we need to follow.
- child = child.rock_ridge.cl_to_moved_dr
- if child is None:
- break
-
- # We found the last child we are looking for; return it.
- if not splitpath:
- return child
-
- if not child.is_dir():
- break
- entry = child
- currpath = splitpath.pop(0).decode('utf-8').encode(encoding)
-
- raise pycdlibexception.PyCdlibInvalidInput('Could not find path')
-
- @functools.lru_cache(maxsize=256)
- def _find_joliet_record(self, joliet_path, encoding='utf-16_be'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- An internal method to find a directory record on the ISO given a Joliet
- path. If the entry is found, it returns the directory record object
- corresponding to that entry. If the entry could not be found, a
- pycdlibexception.PyCdlibInvalidInput is raised.
-
- Parameters:
- joliet_path - The Joliet path to lookup.
- encoding - The string encoding used for the path.
- Returns:
- The directory record entry representing the entry on the ISO.
- """
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInternalError('Joliet path requested on non-Joliet ISO')
- return _find_dr_record_by_name(self.joliet_vd, joliet_path, encoding)
-
- @functools.lru_cache(maxsize=256)
- def _find_udf_record(self, udf_path):
- # type: (bytes) -> Tuple[Optional[udfmod.UDFFileIdentifierDescriptor], udfmod.UDFFileEntry]
- """
- An internal method to find a directory record on the ISO given a UDF
- path. If the entry is found, it returns the directory record object
- corresponding to that entry. If the entry could not be found, a
- pycdlibexception.PyCdlibInvalidInput is raised.
-
- Parameters:
- udf_path - The UDF path to lookup.
- Returns:
- The UDF File Entry representing the entry on the ISO.
- """
- # If the path is just the slash, return the root directory.
- if udf_path == b'/':
- return None, self.udf_root # type: ignore
-
- splitpath = utils.split_path(udf_path)
-
- currpath = splitpath.pop(0)
-
- entry = self.udf_root
-
- while entry is not None:
- child = entry.find_file_ident_desc_by_name(currpath)
-
- # We found the last child we are looking for; return it.
- if not splitpath:
- return child, child.file_entry # type: ignore
-
- if not child.is_dir():
- break
- entry = child.file_entry
- currpath = splitpath.pop(0)
-
- raise pycdlibexception.PyCdlibInvalidInput('Could not find path')
-
- def _iso_name_and_parent_from_path(self, iso_path):
- # type: (bytes) -> Tuple[bytes, dr.DirectoryRecord]
- """
- An internal method to find the parent directory record and name given an
- ISO path. If the parent is found, return a tuple containing the
- basename of the path and the parent directory record object.
-
- Parameters:
- iso_path - The absolute ISO path to the entry on the ISO.
- Returns:
- A tuple containing just the name of the entry and a Directory Record
- object representing the parent of the entry.
- """
-
- splitpath = utils.split_path(iso_path)
- name = splitpath.pop()
-
- parent = self._find_iso_record(b'/' + b'/'.join(splitpath))
-
- return (name.decode('utf-8').encode('utf-8'), parent)
-
- def _joliet_name_and_parent_from_path(self, joliet_path):
- # type: (bytes) -> Tuple[bytes, dr.DirectoryRecord]
- """
- An internal method to find the parent directory record and name given a
- Joliet path. If the parent is found, return a tuple containing the
- basename of the path and the parent directory record object.
-
- Parameters:
- joliet_path - The absolute Joliet path to the entry on the ISO.
- Returns:
- A tuple containing just the name of the entry and a Directory Record
- object representing the parent of the entry.
- """
-
- splitpath = utils.split_path(joliet_path)
- name = splitpath.pop()
-
- if len(name) > 64:
- raise pycdlibexception.PyCdlibInvalidInput('Joliet names can be a maximum of 64 characters')
- parent = self._find_joliet_record(b'/' + b'/'.join(splitpath))
-
- return (name.decode('utf-8').encode('utf-16_be'), parent)
-
- def _udf_name_and_parent_from_path(self, udf_path):
- # type: (bytes) -> Tuple[bytes, udfmod.UDFFileEntry]
- """
- An internal method to find the parent directory record and name given a
- UDF path. If the parent is found, return a tuple containing the
- basename of the path and the parent UDF File Entry object.
-
- Parameters:
- udf_path - The absolute UDF path to the entry on the ISO.
- Returns:
- A tuple containing just the name of the entry and a UDF File Entry
- object representing the parent of the entry.
- """
- splitpath = utils.split_path(udf_path)
- name = splitpath.pop()
- (parent_ident_unused, parent) = self._find_udf_record(b'/' + b'/'.join(splitpath))
-
- return (name.decode('utf-8').encode('utf-8'), parent)
-
- def _set_rock_ridge(self, rr):
- # type: (str) -> None
- """
- An internal method to set the Rock Ridge version of the ISO given the
- Rock Ridge version of the previous entry.
-
- Parameters:
- rr - The version of rr from the last directory record.
- Returns:
- Nothing.
- """
- # We don't allow mixed Rock Ridge versions on the ISO, so apply some
- # checking. If the current overall Rock Ridge version on the ISO is
- # None, we upgrade it to whatever version we were given. Once we have
- # seen a particular version, we only allow records of that version or
- # None (to account for dotdot records which have no Rock Ridge).
- if not self.rock_ridge:
- self.rock_ridge = rr
- else:
- for ver in ('1.09', '1.10', '1.12'):
- if self.rock_ridge == ver:
- if rr and rr != ver:
- raise pycdlibexception.PyCdlibInvalidISO('Inconsistent Rock Ridge versions on the ISO!')
-
- def _get_iso_size(self):
- # type: () -> int
- """
- An internal method to get the ISO size. This is more complicated than
- you might think due to Windows. There, if you try to open a 'raw'
- device, you can only seek on 2048-byte boundaries (and you can't seek to
- the END). We first try to seek to the end, which is the most efficient
- way to do it. If that fails, we fall back to seeking and reading until
- we get empty data, which signals the end of the ISO.
-
- Parameters:
- None.
- Returns:
- The size of the ISO in bytes.
- """
- old = self._cdfp.tell()
- try:
- self._cdfp.seek(0, os.SEEK_END)
- ret = self._cdfp.tell()
- self._cdfp.seek(old)
- return ret
- except OSError:
- pass
-
- # When reading a raw Windows device, we cannot seek to the end to find
- # the length. Instead, we start right at the beginning of the ISO and
- # seek by 1MB at a time to find the end of it. This meets the Windows
- # requirement that we seek by a 512-byte aligned value while also
- # having decent performance. Once we find the 1MB boundary, we back
- # up and find the real 2048-byte boundary.
- bs = 1048576
- one_mb_block = 0
- while True:
- self._cdfp.seek(one_mb_block * bs)
- data = self._cdfp.read(1)
- if len(data) != 1:
- break
- one_mb_block += 1
- if one_mb_block > 0:
- one_mb_block -= 1
- extent = (one_mb_block * bs) // 2048
- while True:
- self._cdfp.seek(extent * 2048)
- data = self._cdfp.read(2048)
- if len(data) != 2048:
- break
- extent += 1
-
- self._cdfp.seek(old)
- return extent * 2048
-
- def _walk_directories(self, vd, extent_to_ptr, extent_to_inode,
- path_table_records):
- # type: (headervd.PrimaryOrSupplementaryVD, Dict[int, path_table_record.PathTableRecord], Dict[int, inode.Inode], List[path_table_record.PathTableRecord]) -> Tuple[int, int]
- """
- An internal method to walk the directory records in a volume descriptor,
- starting with the root. For each child in the directory record,
- create a new dr.DirectoryRecord object and append it to the parent.
-
- Parameters:
- vd - The volume descriptor to walk.
- extent_to_ptr - A dictionary mapping extents to PTRs.
- extent_to_inode - A dictionary mapping extents to Inodes.
- path_table_records - The list of path table records.
- Returns:
- The interchange level that this ISO conforms to.
- """
- cdfp = self._cdfp
- iso_file_length = self._get_iso_size()
-
- all_extent_to_dr = {} # type: Dict[int, dr.DirectoryRecord]
- is_pvd = vd.is_pvd()
- root_dir_record = vd.root_directory_record()
- root_dir_record.set_ptr(path_table_records[0])
- interchange_level = 1
- parent_links = []
- child_links = []
- lastbyte = 0
- dirs = collections.deque([root_dir_record])
- while dirs:
- dir_record = dirs.popleft()
-
- self._seek_to_extent(dir_record.extent_location())
- length = dir_record.get_data_length()
- offset = 0
- last_record = None # type: Optional[dr.DirectoryRecord]
- data = cdfp.read(length)
- while offset < length:
- if offset > (len(data) - 1):
- # The data we read off of the ISO was shorter than what we
- # expected. The ISO is corrupt, throw an error.
- raise pycdlibexception.PyCdlibInvalidISO('Invalid directory record')
- lenbyte = bytearray([data[offset]])[0]
- if lenbyte == 0:
- # If we saw a zero length, this is probably the padding for
- # the end of this extent. Move the offset to the start of
- # the next extent.
- padsize = self.logical_block_size - (offset % self.logical_block_size)
- if data[offset:offset + padsize] != b'\x00' * padsize:
- # For now we are pedantic, and throw an exception if the
- # padding bytes are not all zero. We may have to loosen
- # this check depending on what we see in the wild.
- raise pycdlibexception.PyCdlibInvalidISO('Invalid padding on ISO')
-
- offset = offset + padsize
- continue
-
- new_record = dr.DirectoryRecord()
- rr = new_record.parse(vd, data[offset:offset + lenbyte],
- dir_record)
- offset += lenbyte
-
- self._set_rock_ridge(rr)
-
- # Cache some properties of this record for later use.
- is_symlink = new_record.is_symlink()
- dots = new_record.is_dot() or new_record.is_dotdot()
- rr_cl = new_record.rock_ridge is not None and new_record.rock_ridge.child_link_record_exists()
- is_dir = new_record.is_dir()
- data_length = new_record.get_data_length()
- new_extent_loc = new_record.extent_location()
-
- if is_pvd and not dots and not rr_cl and not is_symlink and new_extent_loc not in all_extent_to_dr:
- all_extent_to_dr[new_extent_loc] = new_record
-
- # Some ISOs use random extent locations for zero-length files.
- # Thus, it is not valid for us to link zero-length files to
- # other files, as the linkage will be essentially random.
- # Ignore zero-length files (including symlinks) for linkage.
- # We don't do the lastbyte calculation on zero-length files for
- # the same reason.
- if not is_dir:
- len_to_use = data_length
- extent_to_use = new_extent_loc
- # An important side-effect of this is that zero-length files
- # or symlinks get an inode, but it is always set to length 0
- # and location 0 and not actually written out. This is so
- # that we can 'link' everything through the Inode.
- if len_to_use == 0 or is_symlink:
- len_to_use = 0
- extent_to_use = 0
-
- # Directory Records that point to the El Torito Boot Catalog
- # do not get Inodes since all of that is handled in-memory.
- if self.eltorito_boot_catalog is not None and extent_to_use == self.eltorito_boot_catalog.extent_location():
- self.eltorito_boot_catalog.add_dirrecord(new_record)
- else:
- # For real files, create an inode that points to the
- # location on disk.
- if extent_to_use in extent_to_inode:
- ino = extent_to_inode[extent_to_use]
- else:
- ino = inode.Inode()
- ino.parse(extent_to_use, len_to_use, cdfp,
- self.logical_block_size)
- extent_to_inode[extent_to_use] = ino
- self.inodes.append(ino)
-
- ino.linked_records.append((new_record, vd == self.pvd))
- new_record.inode = ino
-
- new_end = extent_to_use * self.logical_block_size + len_to_use
- if new_end > iso_file_length:
- # The end of the file is beyond the size of the ISO.
- # Since this can't be true, truncate the file size.
- if new_record.inode is not None:
- new_record.inode.data_length = iso_file_length - extent_to_use * self.logical_block_size
- for rec, is_pvd in new_record.inode.linked_records:
- rec.set_data_length(new_end)
- else:
- # The new end is still within the file size, but the PVD
- # size is wrong. Set the lastbyte appropriately, which
- # will eventually be used to fix the PVD size.
- lastbyte = max(lastbyte, new_end)
-
- if new_record.rock_ridge is not None and new_record.rock_ridge.dr_entries.ce_record is not None:
- ce_record = new_record.rock_ridge.dr_entries.ce_record
- orig_pos = cdfp.tell()
- self._seek_to_extent(ce_record.bl_cont_area)
- cdfp.seek(ce_record.offset_cont_area, os.SEEK_CUR)
- con_block = cdfp.read(ce_record.len_cont_area)
- new_record.rock_ridge.parse(con_block, False,
- new_record.rock_ridge.bytes_to_skip,
- True, new_record.file_identifier())
- cdfp.seek(orig_pos)
- block = self.pvd.track_rr_ce_entry(ce_record.bl_cont_area,
- ce_record.offset_cont_area,
- ce_record.len_cont_area)
- new_record.rock_ridge.update_ce_block(block)
-
- if rr_cl:
- child_links.append(new_record)
-
- if is_dir:
- if new_record.rock_ridge is not None and new_record.rock_ridge.relocated_record():
- self._rr_moved_record = new_record
-
- if new_record.is_dotdot() and new_record.rock_ridge is not None and new_record.rock_ridge.parent_link_record_exists():
- # Make sure to mark a dotdot record with a parent link
- # record in the parent_links list for later linking.
- parent_links.append(new_record)
- if not dots and not rr_cl:
- dirs.append(new_record)
- new_record.set_ptr(extent_to_ptr[new_extent_loc])
-
- if new_record.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Trying to track child with no parent')
- try_long_entry = False
- try:
- new_record.parent.track_child(new_record,
- self.logical_block_size)
- except pycdlibexception.PyCdlibInvalidInput:
- # dir_record.track_child() may throw a PyCdlibInvalidInput
- # if it was given a duplicate child. However, we allow
- # duplicate children if and only if this record is a file
- # and the last file has the same name; this represents a
- # very large file.
- if new_record.is_dir() or last_record is None or last_record.file_identifier() != new_record.file_identifier():
- raise
-
- try_long_entry = True
-
- if try_long_entry:
- new_record.parent.track_child(new_record,
- self.logical_block_size, True)
-
- if is_pvd:
- if new_record.is_dir():
- new_level = _interchange_level_from_directory(new_record.file_identifier())
- else:
- new_level = _interchange_level_from_filename(new_record.file_identifier())
- interchange_level = max(interchange_level, new_level)
-
- last_record = new_record
-
- for pl in parent_links:
- if pl.rock_ridge is not None:
- pl.rock_ridge.parent_link = all_extent_to_dr[pl.rock_ridge.parent_link_extent()]
-
- for cl in child_links:
- if cl.rock_ridge is not None:
- cl.rock_ridge.cl_to_moved_dr = all_extent_to_dr[cl.rock_ridge.child_link_extent()]
- if cl.rock_ridge.cl_to_moved_dr.rock_ridge is not None:
- cl.rock_ridge.cl_to_moved_dr.rock_ridge.moved_to_cl_dr = cl
-
- return interchange_level, lastbyte
-
- def _parse_path_table(self, ptr_size, extent):
- # type: (int, int) -> Tuple[List[path_table_record.PathTableRecord], Dict[int, path_table_record.PathTableRecord]]
- """
- An internal method to parse a path table on an ISO. For each path
- table entry found, a Path Table Record object is created and added to
- the output list.
-
- Parameters:
- ptr_size - The size of the PTR table to read.
- extent - The extent at which this path table record starts.
- Returns:
- A tuple consisting of the list of path table record entries and a
- dictionary of the extent locations to the path table record entries.
- """
- self._seek_to_extent(extent)
- old = self._cdfp.tell()
- data = self._cdfp.read(ptr_size)
- offset = 0
- out = []
- extent_to_ptr = {}
- while offset < ptr_size:
- ptr = path_table_record.PathTableRecord()
- len_di_byte = bytearray([data[offset]])[0]
- read_len = path_table_record.PathTableRecord.record_length(len_di_byte)
-
- ptr.parse(data[offset:offset + read_len])
- out.append(ptr)
- extent_to_ptr[ptr.extent_location] = ptr
- offset += read_len
-
- self._cdfp.seek(old)
- return out, extent_to_ptr
-
- def _check_and_parse_eltorito(self, br):
- # type: (headervd.BootRecord) -> None
- """
- An internal method to examine a Boot Record and see if it is an
- El Torito Boot Record. If it is, parse the El Torito Boot Catalog,
- verification entry, initial entry, and any additional section entries.
-
- Parameters:
- br - The boot record to examine for an El Torito signature.
- Returns:
- Nothing.
- """
- if br.boot_system_identifier != b'EL TORITO SPECIFICATION'.ljust(32, b'\x00'):
- return
-
- if self.eltorito_boot_catalog is not None:
- raise pycdlibexception.PyCdlibInvalidISO('Only one El Torito boot record is allowed')
-
- # According to the El Torito specification, section 2.0, the El
- # Torito boot record must be at extent 17.
- if br.extent_location() != 17:
- raise pycdlibexception.PyCdlibInvalidISO('El Torito Boot Record must be at extent 17')
-
- # Once we have verified that the BootRecord is an El Torito one and that
- # it is sane, we parse the El Torito Boot Catalog.
-
- self.eltorito_boot_catalog = eltorito.EltoritoBootCatalog(br)
- # While it is not entirely clear in the El Torito spec, empirical
- # evidence suggests that the boot catalog extent is always
- # little-endian, even on big-endian systems
- eltorito_boot_catalog_extent, = struct.unpack_from('<L',
- br.boot_system_use[:4],
- 0)
-
- old = self._cdfp.tell()
- self._seek_to_extent(eltorito_boot_catalog_extent)
- data = self._cdfp.read(32)
- while not self.eltorito_boot_catalog.parse(data):
- data = self._cdfp.read(32)
- self._cdfp.seek(old)
-
- def _udf_assign_extents(self, udf_files, current_extent):
- # type: (List[inode.Inode], int) -> Tuple[int, int]
- """
- An internal method to assign UDF extents when reshuffling.
-
- Parameters:
- udf_files - The list of UDF Inodes that will need extents assigned.
- current_extent - The current extent being assigned.
- Returns:
- A tuple where the first entry is the new current extent, and the
- second entry is the new partition start.
- """
- if current_extent > 32:
- # There is no *requirement* in the UDF specification that the UDF
- # Volume Descriptor Sequence starts at extent 32. It can start
- # anywhere between extents 16 and 256, as long as the ISO9660
- # volume descriptors, the UDF Bridge Volume Recognition Sequence,
- # Main Volume Descriptor Sequence, Reserve Volume Descriptor
- # Sequence, and Logical Volume Integrity Sequence all fit, in that
- # order. The only way that all of these volume descriptors would
- # not fit between extents 16 and 32 is in the case of many
- # duplicate PVDs, many VDSTs, or similar. Since that is unlikely,
- # for now we maintain compatibility with genisoimage and force the
- # UDF Main Descriptor Sequence to start at 32. We can change this
- # later if needed.
- raise pycdlibexception.PyCdlibInternalError('Too many ISO9660 volume descriptors to fit UDF')
-
- self.udf_main_descs.assign_desc_extents(32)
-
- # ECMA TR-071 2.6 says that the volume sequence will be exactly 16
- # extents long, and we know we started at 32, so make it exactly 48.
- self.udf_reserve_descs.assign_desc_extents(48)
-
- # ECMA TR-071 2.6 says that the volume sequence will be exactly 16
- # extents long, and we know we started at 48, so make it exactly 64.
- current_extent = 64
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.set_extent_location(current_extent)
- self.udf_main_descs.logical_volumes[0].set_integrity_location(current_extent)
- self.udf_reserve_descs.logical_volumes[0].set_integrity_location(current_extent)
- current_extent += 1
-
- if self.udf_logical_volume_integrity_terminator is not None:
- self.udf_logical_volume_integrity_terminator.set_extent_location(current_extent)
- current_extent += 1
-
- if len(self.udf_anchors) != 2:
- raise pycdlibexception.PyCdlibInternalError('Expected 2 UDF anchors')
-
- # The first UDF anchor is hard-coded at extent 256. We assign the
- # other anchor later, since it needs to be the last extent.
- current_extent = 256
- self.udf_anchors[0].set_extent_location(current_extent,
- self.udf_main_descs.pvds[0].extent_location(),
- self.udf_reserve_descs.pvds[0].extent_location())
- current_extent += 1
-
- # Now assign the UDF File Set Descriptor to the beginning of the
- # partition.
- part_start = current_extent
- self.udf_file_set.set_extent_location(part_start)
- self.udf_main_descs.partitions[0].set_start_location(part_start)
- self.udf_reserve_descs.partitions[0].set_start_location(part_start)
- current_extent += 1
-
- if self.udf_file_set_terminator is not None:
- self.udf_file_set_terminator.set_extent_location(current_extent,
- current_extent - part_start)
- current_extent += 1
-
- # Assignment of extents to UDF is complicated. UDF filesystems are
- # arranged by having one extent containing a File Entry that describes
- # a directory or a file, followed by an extent that contains the
- # entries in the case of a directory. All File Entries and entries
- # containing File Identifier Descriptors are arranged ahead of File
- # Entries for files. The implementation below alternates assignment to
- # File Entries and File Descriptores for all directories, and then
- # then assigns to all files. Note that data for files is assigned in
- # the 'normal' file assignment below.
-
- # First assign directories.
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInternalError('ISO has UDF but no UDF root; this should never happen')
- udf_file_assign_list = []
- udf_file_entries = collections.deque([(self.udf_root, None)]) # type: Deque[Tuple[udfmod.UDFFileEntry, Optional[udfmod.UDFFileIdentifierDescriptor]]]
- while udf_file_entries:
- udf_file_entry, fi_desc = udf_file_entries.popleft()
-
- # In theory we should check for and skip the work for files and
- # symlinks, but they will never be added to 'udf_file_entries'
- # to begin with so we can safely ignore them.
-
- # Set the location that the File Entry lives at, and update the
- # File Identifier Descriptor that points to it (for all but the
- # root).
- udf_file_entry.set_extent_location(current_extent,
- current_extent - part_start)
- if fi_desc is not None:
- fi_desc.set_icb(current_extent, current_extent - part_start)
- current_extent += 1
-
- # Now assign where the File Entry points to; for files this is
- # overwritten later, but for directories this tells us where to
- # find the extent containing the list of File Identifier
- # Descriptors that are in this directory.
- udf_file_entry.set_data_location(current_extent,
- current_extent - part_start)
- offset = 0
- for d in udf_file_entry.fi_descs:
- if offset >= self.logical_block_size:
- # The offset has spilled over into a new extent. Increase
- # the current extent by one, and update the offset. Note
- # that the offset does not go to 0, since UDF allows File
- # Identifier Descs to span extents. Instead, it is the
- # current offset minus the size of a block (say 2050 - 2048,
- # leaving us at offset 2).
- current_extent += 1
- offset = offset - self.logical_block_size
-
- d.set_extent_location(current_extent,
- current_extent - part_start)
- if not d.is_parent() and d.file_entry is not None:
- if d.is_dir():
- udf_file_entries.append((d.file_entry, d))
- else:
- udf_file_assign_list.append((d.file_entry, d))
- offset += udfmod.UDFFileIdentifierDescriptor.length(len(d.fi))
-
- if offset > self.logical_block_size:
- current_extent += 1
-
- current_extent += 1
-
- # Now assign files (this includes symlinks).
- udf_file_entry_inodes_assigned = set()
- for udf_file_assign_entry, fi_desc in udf_file_assign_list:
- if udf_file_assign_entry is None or fi_desc is None:
- continue
-
- if udf_file_assign_entry.inode is not None and id(udf_file_assign_entry.inode) in udf_file_entry_inodes_assigned:
- continue
-
- udf_file_assign_entry.set_extent_location(current_extent,
- current_extent - part_start)
- fi_desc.set_icb(current_extent, current_extent - part_start)
-
- if udf_file_assign_entry.inode is not None:
- # The data location for files will be set later.
- if udf_file_assign_entry.inode.get_data_length() > 0:
- udf_files.append(udf_file_assign_entry.inode)
- for rec, pvd_unused in udf_file_assign_entry.inode.linked_records:
- if isinstance(rec, udfmod.UDFFileEntry):
- rec.set_extent_location(current_extent,
- current_extent - part_start)
- if rec.file_ident is not None:
- rec.file_ident.set_icb(current_extent,
- current_extent - part_start)
-
- udf_file_entry_inodes_assigned.add(id(udf_file_assign_entry.inode))
-
- current_extent += 1
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_contents_use.unique_id = current_extent
-
- return current_extent, part_start
-
- def _set_inode(self, ino, current_extent, part_start):
- # type: (inode.Inode, int, int) -> int
- """
- An internal function to set the location of an inode and update the
- metadata of all records attached to it.
-
- Parameters:
- ino - The inode to update.
- current_extent - The extent to set the inode to.
- part_start - The start of the partition that the inode is on.
- Returns:
- The new extent location.
- """
- if len(self.udf_anchors) > 2 and current_extent == self.pvd.space_size - 256:
- current_extent += 1
-
- ino.set_extent_location(current_extent)
- for rec, pvd_unused in ino.linked_records:
- rec.set_data_location(current_extent,
- current_extent - part_start)
-
- current_extent += utils.ceiling_div(ino.get_data_length(),
- self.logical_block_size)
- return current_extent
-
- def _reshuffle_extents(self):
- # type: () -> None
- """
- An internal method that is one of the keys of PyCdlib's ability to keep
- the in-memory metadata consistent at all times. After making any
- changes to the ISO, most API calls end up calling this method. This
- method will run through the entire ISO, assigning extents to each of
- the pieces of the ISO that exist. This includes the Primary Volume
- Descriptor (which is fixed at extent 16), the Boot Records (including
- El Torito), the Supplementary Volume Descriptors (including Joliet),
- the Volume Descriptor Terminators, the Version Descriptor, the Primary
- Volume Descriptor Path Table Records (little and big endian), the
- Supplementary Volume Descriptor Path Table Records (little and big
- endian), the Primary Volume Descriptor directory records, the
- Supplementary Volume Descriptor directory records, the Rock Ridge ER
- sector, the El Torito Boot Catalog, the El Torito Initial Entry, the
- various UDF metadata entries, and the data for files.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- current_extent = 16
- for pvd in self.pvds:
- pvd.set_extent_location(current_extent)
- current_extent += 1
-
- for br in self.brs:
- br.set_extent_location(current_extent)
- current_extent += 1
-
- for svd in self.svds:
- svd.set_extent_location(current_extent)
- current_extent += 1
-
- for vdst in self.vdsts:
- vdst.set_extent_location(current_extent)
- current_extent += 1
-
- if self._has_udf:
- for bea in self.udf_beas:
- bea.set_extent_location(current_extent)
- current_extent += 1
-
- for boot in self.udf_boots:
- boot.set_extent_location(current_extent)
- current_extent += 1
-
- self.udf_nsr.set_extent_location(current_extent)
- current_extent += 1
-
- for tea in self.udf_teas:
- tea.set_extent_location(current_extent)
- current_extent += 1
-
- if self.version_vd is not None:
- self.version_vd.set_extent_location(current_extent)
- current_extent += 1
-
- part_start = 0
-
- udf_files = [] # type: List[inode.Inode]
- if self._has_udf:
- current_extent, part_start = self._udf_assign_extents(udf_files, current_extent)
-
- # Next up, put the path table records in the right place.
- for pvd in self.pvds:
- pvd.path_table_location_le = current_extent
- current_extent += self.pvd.path_table_num_extents
-
- for pvd in self.pvds:
- pvd.path_table_location_be = current_extent
- current_extent += self.pvd.path_table_num_extents
-
- if self.enhanced_vd is not None:
- self.enhanced_vd.path_table_location_le = self.pvd.path_table_location_le
- self.enhanced_vd.path_table_location_be = self.pvd.path_table_location_be
-
- if self.joliet_vd is not None:
- self.joliet_vd.path_table_location_le = current_extent
- current_extent += self.joliet_vd.path_table_num_extents
- self.joliet_vd.path_table_location_be = current_extent
- current_extent += self.joliet_vd.path_table_num_extents
-
- self.pvd.clear_rr_ce_entries()
- current_extent, pvd_files = _reassign_vd_dirrecord_extents(self.pvd,
- current_extent)
-
- joliet_files = [] # type: List[inode.Inode]
- if self.joliet_vd is not None:
- current_extent, joliet_files = _reassign_vd_dirrecord_extents(self.joliet_vd,
- current_extent)
-
- # The rock ridge 'ER' sector must be after all of the directory
- # entries but before the file contents.
- rr = self.pvd.root_directory_record().children[0].rock_ridge
- if rr is not None and rr.dr_entries.ce_record is not None:
- rr.dr_entries.ce_record.update_extent(current_extent)
- current_extent += 1
-
- if len(self.udf_anchors) > 2:
- self.udf_anchors[1].set_extent_location(self.pvd.space_size - 256,
- self.udf_main_descs.pvds[0].extent_location(),
- self.udf_reserve_descs.pvds[0].extent_location())
-
- linked_inodes = set()
- if self.eltorito_boot_catalog is not None:
- self.eltorito_boot_catalog.update_catalog_extent(current_extent)
- for rec in self.eltorito_boot_catalog.dirrecords:
- rec.set_data_location(current_extent, current_extent - part_start)
-
- current_extent += utils.ceiling_div(self.eltorito_boot_catalog.dirrecords[0].get_data_length(),
- self.logical_block_size)
-
- class _EltoritoEncapsulation:
- """
- An internal class to encapsulate an El Torito Entry object with
- additional necessary metadata for sorting.
- """
- def __init__(self, entry, platform_id, name):
- self.entry = entry
- self.platform_id = platform_id
- self.name = name
-
- def __lt__(self, other):
- return self.name < other.name
-
- def _add_entry_to_enc_list(enc_to_update, entry, platform_id):
- # type: (List[_EltoritoEncapsulation], eltorito.EltoritoEntry, int) -> None
- added_enc = False
- for rec, is_pvd in entry.inode.linked_records:
- if not is_pvd:
- continue
-
- if not isinstance(rec, udfmod.UDFFileEntry) and not isinstance(rec, dr.DirectoryRecord):
- continue
-
- enc = _EltoritoEncapsulation(entry, platform_id, rec.file_identifier())
- bisect.insort_right(enc_to_update, enc)
- added_enc = True
-
- if not added_enc:
- # In this case, the entry wasn't linked into the PVD
- # filesystem at all. Just add an entry with a dummy name
- # that is guaranteed to sort first.
- enc = _EltoritoEncapsulation(entry, platform_id, b'AAAAAAAA.;1')
- bisect.insort_right(enc_to_update, enc)
-
- enc_to_update = [] # type: List[_EltoritoEncapsulation]
- _add_entry_to_enc_list(enc_to_update,
- self.eltorito_boot_catalog.initial_entry,
- self.eltorito_boot_catalog.validation_entry.platform_id)
-
- for sec in self.eltorito_boot_catalog.sections:
- for entry in sec.section_entries:
- _add_entry_to_enc_list(enc_to_update, entry, sec.platform_id)
-
- for entry in self.eltorito_boot_catalog.standalone_entries:
- _add_entry_to_enc_list(enc_to_update, entry,
- self.eltorito_boot_catalog.validation_entry.platform_id)
-
- num_seen_efi = 0
- for enc in enc_to_update:
- if id(enc.entry.inode) in linked_inodes:
- continue
-
- enc.entry.set_data_location(current_extent,
- current_extent - part_start)
-
- if self.isohybrid_mbr is not None:
- if enc.platform_id == 0xef:
- if num_seen_efi == 0:
- self.isohybrid_mbr.update_efi(current_extent,
- entry.sector_count,
- self.pvd.space_size * self.logical_block_size)
- elif num_seen_efi == 1:
- self.isohybrid_mbr.update_mac(current_extent,
- entry.sector_count)
- else:
- raise pycdlibexception.PyCdlibInternalError('Only expected two EFI sections')
- num_seen_efi += 1
- elif enc.platform_id == 0:
- self.isohybrid_mbr.update_rba(current_extent)
-
- current_extent = self._set_inode(enc.entry.inode, current_extent,
- part_start)
- linked_inodes.add(id(enc.entry.inode))
-
- for ino in pvd_files + joliet_files + udf_files:
- if id(ino) in linked_inodes:
- # We've already assigned an extent because it was linked to an
- # earlier entry.
- continue
-
- current_extent = self._set_inode(ino, current_extent, part_start)
-
- linked_inodes.add(id(ino))
-
- if self.enhanced_vd is not None:
- loc = self.pvd.root_directory_record().extent_location()
- self.enhanced_vd.root_directory_record().set_data_location(loc, loc)
-
- if self.udf_anchors:
- self.udf_anchors[-1].set_extent_location(current_extent,
- self.udf_main_descs.pvds[0].extent_location(),
- self.udf_reserve_descs.pvds[0].extent_location())
-
- if current_extent > self.pvd.space_size:
- raise pycdlibexception.PyCdlibInternalError('Assigned an extent beyond the ISO (%d > %d)' % (current_extent, self.pvd.space_size))
-
- self._needs_reshuffle = False
-
- def _add_child_to_dr(self, child):
- # type: (dr.DirectoryRecord) -> int
- """
- An internal method to add a child to a directory record, expanding the
- space in the Volume Descriptor(s) if necessary.
-
- Parameters:
- child - The new child.
- Returns:
- The number of bytes to add for this directory record (this may be zero).
- """
- if child.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Trying to add child without a parent')
-
- try_long_entry = False
- try:
- ret = child.parent.add_child(child, self.logical_block_size)
- except pycdlibexception.PyCdlibInvalidInput:
- # dir_record.add_child() may throw a PyCdlibInvalidInput if it was
- # given a duplicate child. However, we allow duplicate children if
- # and only the last child is the same; this represents a very large
- # file.
- if not child.is_dir():
- try_long_entry = True
- else:
- raise
-
- if try_long_entry:
- ret = child.parent.add_child(child, self.logical_block_size, True)
-
- # The add_child() method returns True if the parent needs another extent
- # in order to fit the directory record for this child.
- if ret:
- return self.logical_block_size
-
- return 0
-
- def _remove_child_from_dr(self, child, index):
- # type: (dr.DirectoryRecord, int) -> int
- """
- An internal method to remove a child from a directory record, shrinking
- the space in the Volume Descriptor if necessary.
-
- Parameters:
- child - The child to remove.
- index - The index of the child into the parent's child array.
- Returns:
- The number of bytes to remove for this directory record (this may be zero).
- """
-
- if child.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Trying to remove child from non-existent parent')
-
- self._find_iso_record.cache_clear() # pylint: disable=no-member
- self._find_rr_record.cache_clear() # pylint: disable=no-member
- self._find_joliet_record.cache_clear() # pylint: disable=no-member
-
- # The remove_child() method returns True if the parent no longer needs
- # the extent that the directory record for this child was on.
- if child.parent.remove_child(child, index, self.logical_block_size):
- return self.logical_block_size
-
- return 0
-
- def _add_to_ptr_size(self, ptr):
- # type: (path_table_record.PathTableRecord) -> int
- """
- An internal method to add a PTR to a VD, adding space to the VD if
- necessary.
-
- Parameters:
- ptr - The PTR to add to the vd.
- Returns:
- The number of additional bytes that are needed to fit the new PTR
- (this may be zero).
- """
- num_bytes_to_add = 0
- for pvd in self.pvds:
- # The add_to_ptr_size() method returns True if the PVD needs
- # additional space in the PTR to store this directory. We always
- # add 4 additional extents for that (2 for LE, 2 for BE).
- if pvd.add_to_ptr_size(path_table_record.PathTableRecord.record_length(ptr.len_di)):
- num_bytes_to_add += 4 * self.logical_block_size
-
- return num_bytes_to_add
-
- def _remove_from_ptr_size(self, ptr):
- # type: (path_table_record.PathTableRecord) -> int
- """
- An internal method to remove a PTR from a VD, removing space from the
- VD if necessary.
-
- Parameters:
- ptr - The PTR to remove from the VD.
- Returns:
- The number of bytes to remove from the VDs (this may be zero).
- """
- num_bytes_to_remove = 0
- for pvd in self.pvds:
- # The remove_from_ptr_size() method returns True if the PVD no
- # longer needs the extra extents in the PTR that stored this
- # directory. We always remove 4 additional extents for that.
- if pvd.remove_from_ptr_size(path_table_record.PathTableRecord.record_length(ptr.len_di)):
- num_bytes_to_remove += 4 * self.logical_block_size
-
- return num_bytes_to_remove
-
- def _find_or_create_rr_moved(self):
- # type: () -> int
- """
- An internal method to find the /RR_MOVED directory on the ISO. If it
- already exists, the directory record to it is returned. If it doesn't
- yet exist, it is created and the directory record to it is returned.
-
- Parameters:
- None.
- Returns:
- The number of additional bytes needed for the rr_moved directory (this
- may be zero).
- """
-
- if self._rr_moved_record.initialized:
- return 0
-
- if self._rr_moved_name is None:
- self._rr_moved_name = b'RR_MOVED'
- if self._rr_moved_rr_name is None:
- self._rr_moved_rr_name = b'rr_moved'
-
- # No rr_moved found, so we have to create it.
- rec = dr.DirectoryRecord()
- rec.new_dir(self.pvd, self._rr_moved_name,
- self.pvd.root_directory_record(),
- self.pvd.sequence_number(), self.rock_ridge,
- self._rr_moved_rr_name, self.logical_block_size,
- False, False, self.xa, 0o040555, time.time())
- num_bytes_to_add = self._add_child_to_dr(rec)
-
- self._create_dot(self.pvd, rec, self.rock_ridge, self.xa, 0o040555)
- self._create_dotdot(self.pvd, rec, self.rock_ridge, False, self.xa,
- 0o040555)
-
- # We always need to add an entry to the path table record.
- ptr = path_table_record.PathTableRecord()
- ptr.new_dir(self._rr_moved_name)
- num_bytes_to_add += self.logical_block_size + self._add_to_ptr_size(ptr)
-
- rec.set_ptr(ptr)
-
- self._rr_moved_record = rec
-
- return num_bytes_to_add
-
- def _calculate_eltorito_boot_info_table_csum(self, data_fp, data_len):
- # type: (BinaryIO, int) -> int
- """
- An internal method to calculate the checksum for an El Torito Boot Info
- Table. This checksum is a simple 32-bit checksum over all of the data
- in the boot file, starting right after the Boot Info Table itself.
-
- Parameters:
- data_fp - The file object to read the input data from.
- data_len - The length of the input file.
- Returns:
- An integer representing the 32-bit checksum for the boot info table.
- """
- # Read the boot file so we can calculate the checksum over it.
- num_sectors = utils.ceiling_div(data_len, self.logical_block_size)
- csum = 0
- curr_sector = 0
- while curr_sector < num_sectors:
- block = data_fp.read(self.logical_block_size)
- block = block.ljust(2048, b'\x00')
- i = 0
- if curr_sector == 0:
- # The first 64 bytes are not included in the checksum.
- i = 64
- while i < len(block):
- tmp, = struct.unpack_from('<L', block[:i + 4], i)
- csum += tmp
- csum = csum & 0xffffffff
- i += 4
-
- curr_sector += 1
-
- return csum
-
- def _check_for_eltorito_boot_info_table(self, ino):
- # type: (inode.Inode) -> None
- """
- An internal method to check a boot directory record to see if it has
- an El Torito Boot Info Table embedded inside of it.
-
- Parameters:
- ino - The Inode to check for a Boot Info Table.
- Returns:
- Nothing.
- """
- orig = self._cdfp.tell()
- with inode.InodeOpenData(ino, self.logical_block_size) as (data_fp, data_len):
- data_fp.seek(8, os.SEEK_CUR)
- bi_table = eltorito.EltoritoBootInfoTable()
- if bi_table.parse(self.pvd, data_fp.read(eltorito.EltoritoBootInfoTable.header_length()), ino):
- data_fp.seek(-24, os.SEEK_CUR)
- # Do a final check to make sure the checksum matches.
- csum = self._calculate_eltorito_boot_info_table_csum(data_fp,
- data_len)
-
- if csum == bi_table.csum:
- ino.add_boot_info_table(bi_table)
-
- self._cdfp.seek(orig)
-
- def _check_rr_name(self, rr_name):
- # type: (Optional[str]) -> bytes
- """
- An internal method to check whether this ISO requires a Rock Ridge path.
-
- Parameters:
- rr_name - The Rock Ridge name.
- Returns:
- The Rock Ridge name in bytes if this is a Rock Ridge ISO, an empty byte string otherwise.
- """
- if self.rock_ridge:
- if not rr_name:
- raise pycdlibexception.PyCdlibInvalidInput('A rock ridge name must be passed for a rock-ridge ISO')
-
- if rr_name.count('/') != 0:
- raise pycdlibexception.PyCdlibInvalidInput('A rock ridge name must be relative')
-
- return rr_name.encode('utf-8')
-
- if rr_name:
- raise pycdlibexception.PyCdlibInvalidInput('A rock ridge name can only be specified for a rock-ridge ISO')
-
- return b''
-
- def _normalize_joliet_path(self, joliet_path):
- # type: (str) -> bytes
- """
- An internal method to check whether this ISO requires a Joliet path.
- If a Joliet path is required, the path is normalized and returned.
-
- Parameters:
- joliet_path - The joliet_path to normalize (if necessary).
- Returns:
- The normalized joliet_path if this ISO has Joliet, an empty byte string otherwise.
- """
- tmp_path = b''
- if self.joliet_vd is not None:
- if not joliet_path:
- raise pycdlibexception.PyCdlibInvalidInput('A Joliet path must be passed for a Joliet ISO')
- tmp_path = utils.normpath(joliet_path)
- else:
- if joliet_path:
- raise pycdlibexception.PyCdlibInvalidInput('A Joliet path can only be specified for a Joliet ISO')
-
- return tmp_path
-
- def _link_eltorito(self, extent_to_inode):
- # type: (Dict[int, inode.Inode]) -> None
- """
- An internal method to link the El Torito entries into their
- corresponding Directory Records, creating new ones if they are
- 'hidden'. Should only be called on an El Torito ISO.
-
- Parameters:
- extent_to_inode - A map from extents to Inodes.
- Returns:
- Nothing.
- """
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInternalError('Trying to link El Torito entries on a non-El Torito ISO')
-
- entries_to_assign = [self.eltorito_boot_catalog.initial_entry]
- for sec in self.eltorito_boot_catalog.sections:
- for entry in sec.section_entries:
- entries_to_assign.append(entry)
-
- for entry in entries_to_assign:
- entry_extent = entry.get_rba()
- if entry_extent in extent_to_inode:
- ino = extent_to_inode[entry_extent]
- else:
- ino = inode.Inode()
- ino.parse(entry_extent, entry.length(), self._cdfp,
- self.logical_block_size)
- extent_to_inode[entry_extent] = ino
- self.inodes.append(ino)
-
- ino.linked_records.append((entry, False))
- entry.set_inode(ino)
-
- def _parse_udf_vol_descs(self, udf_extent_ad):
- # type: (udfmod.UDFExtentAD) -> udfmod.UDFDescriptorSequence
- """
- An internal method to parse a set of UDF Volume Descriptors.
-
- Parameters:
- udf_extent_ad - The UDF Extent of the anchor volume to parse.
- Returns:
- The UDFDescriptorSequence object that stores parsed objects.
- """
- self._seek_to_extent(udf_extent_ad.extent_location)
- vd_data = self._cdfp.read(udf_extent_ad.extent_length)
-
- return udfmod.parse_udf_vol_descs(vd_data, udf_extent_ad.extent_location,
- self.logical_block_size)
-
- def _parse_udf_descriptors(self):
- # type: () -> None
- """
- An internal method to parse the UDF descriptors on the ISO. This should
- only be called if it the ISO has a valid UDF Volume Recognition Sequence
- at the beginning of the ISO.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- # Parse the anchors. According to ECMA-167, Part 3, 8.4.2.1, there
- # must be anchors recorded in at least two of the three extent locations
- # 256, N-256, and N, where N is the total number of extents on the disc.
- # Note that one some ISOs, the PVD lies about the amount of space, so we
- # actually check what the PVD tells us is the end, but also the end of
- # the physical space on the ISO. We'll preserve as many as we find,
- # with a minimum of two for a valid ISO.
- self._cdfp.seek(0, os.SEEK_END)
- last_physical_extent = (self._cdfp.tell() // self.logical_block_size) - 1
- last_pvd_extent = self.pvd.space_size - 1
- potential_anchor_locations = {256, last_pvd_extent - 256,
- last_pvd_extent, last_physical_extent,
- last_physical_extent - 256}
-
- for loc in potential_anchor_locations:
- self._seek_to_extent(loc)
- potential_anchor_data = self._cdfp.read(self.logical_block_size)
- potential_anchor = udfmod.parse_anchor(potential_anchor_data, loc)
- if potential_anchor is None:
- continue
-
- if len(self.udf_anchors) > 0 and potential_anchor != self.udf_anchors[0]:
- raise pycdlibexception.PyCdlibInvalidISO('Anchor points do not match')
- self.udf_anchors.append(potential_anchor)
-
- if len(self.udf_anchors) < 2:
- raise pycdlibexception.PyCdlibInvalidISO('Expected at least 2 UDF Anchors')
-
- # ECMA-167, Part 3, 8.4.2 says that the anchors identify the main volume
- # descriptor sequence, so look for it here.
-
- # Parse the Main Volume Descriptor Sequence.
- self.udf_main_descs = self._parse_udf_vol_descs(self.udf_anchors[0].main_vd)
-
- # ECMA-167, Part 3, 8.4.2 and 8.4.2.2 says that the anchors *may*
- # identify a reserve volume descriptor sequence. 10.2.3 says that
- # a reserve volume sequence is identified if the length is > 0.
-
- if self.udf_anchors[0].reserve_vd.extent_length > 0:
- # Parse the Reserve Volume Descriptor Sequence.
- self.udf_reserve_descs = self._parse_udf_vol_descs(self.udf_anchors[0].reserve_vd)
-
- # ECMA-167, Part 3, 10.6.12 says that the integrity sequence extent
- # only exists if the length is > 0.
- if self.udf_main_descs.logical_volumes[0].integrity_sequence.extent_length > 0:
- # Parse the Logical Volume Integrity Sequence.
- self._seek_to_extent(self.udf_main_descs.logical_volumes[0].integrity_sequence.extent_location)
- integrity_data = self._cdfp.read(self.udf_main_descs.logical_volumes[0].integrity_sequence.extent_length)
-
- ulvi, ulvi_term = udfmod.parse_logical_volume_integrity(integrity_data,
- self.udf_main_descs.logical_volumes[0].integrity_sequence.extent_location,
- self.logical_block_size)
- self.udf_logical_volume_integrity = ulvi
- self.udf_logical_volume_integrity_terminator = ulvi_term
-
- # Now look for the File Set Descriptor.
- current_extent = self.udf_main_descs.partitions[0].part_start_location
- self._seek_to_extent(current_extent)
- # Read the data for the File Set and File Terminator together
- file_set_and_term_data = self._cdfp.read(2 * self.logical_block_size)
-
- self.udf_file_set, self.udf_file_set_terminator = udfmod.parse_file_set(file_set_and_term_data,
- current_extent,
- self.logical_block_size)
-
- def _walk_udf_directories(self, extent_to_inode):
- # type: (Dict[int, inode.Inode]) -> None
- """
- An internal method to walk a UDF filesystem and add all the metadata to
- this object.
-
- Parameters:
- extent_to_inode - A map from extent numbers to Inodes.
- Returns:
- Nothing.
- """
- part_start = self.udf_main_descs.partitions[0].part_start_location
-
- abs_file_entry_extent = part_start + self.udf_file_set.root_dir_icb.log_block_num
- self._seek_to_extent(abs_file_entry_extent)
- icbdata = self._cdfp.read(self.udf_file_set.root_dir_icb.extent_length)
- self.udf_root = udfmod.parse_file_entry(icbdata,
- abs_file_entry_extent,
- self.udf_file_set.root_dir_icb.log_block_num,
- None)
-
- udf_file_entries = collections.deque([self.udf_root])
- while udf_file_entries:
- udf_file_entry = udf_file_entries.popleft()
-
- if udf_file_entry is None:
- continue
-
- for desc in udf_file_entry.alloc_descs:
- abs_file_ident_extent = part_start + desc.log_block_num
- self._seek_to_extent(abs_file_ident_extent)
- self._cdfp.seek(desc.offset, 1)
- data = self._cdfp.read(desc.extent_length)
- offset = 0
- while offset < len(data):
- current_extent = (abs_file_ident_extent * self.logical_block_size + offset) // self.logical_block_size
-
- file_ident, bytes_forward = udfmod.parse_file_ident(data[offset:],
- current_extent,
- part_start,
- udf_file_entry)
- offset += bytes_forward
-
- if file_ident.is_parent():
- # For a parent, no further work to do.
- udf_file_entry.track_file_ident_desc(file_ident)
- continue
-
- abs_file_entry_extent = part_start + file_ident.icb.log_block_num
- self._seek_to_extent(abs_file_entry_extent)
- icbdata = self._cdfp.read(file_ident.icb.extent_length)
- next_entry = udfmod.parse_file_entry(icbdata,
- abs_file_entry_extent,
- file_ident.icb.log_block_num,
- udf_file_entry)
-
- # For a non-parent, we delay adding this to the list of
- # fi_descs until after we check whether this is a valid
- # entry or not.
- udf_file_entry.track_file_ident_desc(file_ident)
-
- if next_entry is None:
- if file_ident.is_dir():
- raise pycdlibexception.PyCdlibInvalidISO('Empty UDF File Entry for directories are not allowed')
-
- # If the next_entry is None, then we just skip the
- # rest of the code dealing with the entry and the
- # Inode.
- continue
-
- file_ident.file_entry = next_entry
- next_entry.file_ident = file_ident
-
- if file_ident.is_dir():
- udf_file_entries.append(next_entry)
- else:
- if next_entry.get_data_length() > 0:
- abs_file_data_extent = part_start + next_entry.alloc_descs[0].log_block_num
- else:
- abs_file_data_extent = 0
- if self.eltorito_boot_catalog is not None and abs_file_data_extent == self.eltorito_boot_catalog.extent_location():
- self.eltorito_boot_catalog.add_dirrecord(next_entry)
- else:
- if abs_file_data_extent in extent_to_inode:
- ino = extent_to_inode[abs_file_data_extent]
- else:
- ino = inode.Inode()
- ino.parse(abs_file_data_extent,
- next_entry.get_data_length(),
- self._cdfp, self.logical_block_size)
- extent_to_inode[abs_file_data_extent] = ino
- self.inodes.append(ino)
-
- ino.linked_records.append((next_entry, False))
- next_entry.inode = ino
-
- def _open_fp(self, fp):
- # type: (IO) -> None
- """
- An internal method to open an existing ISO for inspection and
- modification. Note that the file object passed in here must stay open
- for the lifetime of this object, as the PyCdlib class uses it internally
- to do writing and reading operations.
-
- Parameters:
- fp - The file object containing the ISO to open up.
- Returns:
- Nothing.
- """
- if hasattr(fp, 'mode') and 'b' not in fp.mode:
- raise pycdlibexception.PyCdlibInvalidInput("The file to open must be in binary mode (add 'b' to the open flags)")
-
- # If this is a Windows platform, and the file-like object name starts
- # with \\.\, this is a "raw" Windows device and we have to treat it
- # specially.
- if sys.platform == 'win32' and hasattr(fp, 'name') and fp.name.startswith("\\\\.\\"):
- fp = utils.Win32RawDevice(fp.name)
-
- self._cdfp = fp
-
- # Get the Primary Volume Descriptor (pvd), the set of Supplementary
- # Volume Descriptors (svds), the set of Volume Partition
- # Descriptors (vpds), the set of Boot Records (brs), and the set of
- # Volume Descriptor Set Terminators (vdsts)
- self._parse_volume_descriptors()
-
- self.logical_block_size = self.pvd.logical_block_size()
-
- old = self._cdfp.tell()
- self._cdfp.seek(0)
- tmp_isohybrid = isohybrid.IsoHybrid()
- if tmp_isohybrid.parse(self._cdfp.read(16 * 2048)):
- if tmp_isohybrid.efi:
- # If we have an EFI partition, we now need to go find the backup
- # LBA and parse that. The backup_lba attribute tells us the
- # location of the GPT Header, which is *after* the backup
- # partition information, so we first parse that, then go find
- # out how many more partitions we need to parse.
- self._cdfp.seek(tmp_isohybrid.primary_gpt.header.backup_lba * 512)
- tmp_isohybrid.parse_secondary_gpt_header(self._cdfp.read(512))
-
- self._cdfp.seek((tmp_isohybrid.secondary_gpt.header.current_lba * 512) - (tmp_isohybrid.secondary_gpt.header.num_parts * 128))
- tmp_isohybrid.parse_secondary_gpt_partitions(self._cdfp.read(tmp_isohybrid.secondary_gpt.header.num_parts * 128))
-
- # We only save the object if it turns out to be a valid IsoHybrid.
- self.isohybrid_mbr = tmp_isohybrid
- self._cdfp.seek(old)
-
- if self.pvd.application_use[141:149] == b'CD-XA001':
- self.xa = True
-
- for br in self.brs:
- self._check_and_parse_eltorito(br)
-
- # Now that we have the PVD, parse the Path Tables according to Ecma-119
- # section 9.4. We want to ensure that the big endian versions agree
- # with the little endian ones (to make sure it is a valid ISO).
-
- # Little Endian first.
- le_ptrs, extent_to_ptr = self._parse_path_table(self.pvd.path_table_size(),
- self.pvd.path_table_location_le)
-
- # Big Endian next.
- tmp_be_ptrs, e_unused = self._parse_path_table(self.pvd.path_table_size(),
- self.pvd.path_table_location_be)
-
- for index, ptr in enumerate(le_ptrs):
- if not ptr.equal_to_be(tmp_be_ptrs[index]):
- raise pycdlibexception.PyCdlibInvalidISO('Little-endian and big-endian path table records do not agree')
-
- self.interchange_level = 1
- for svd in self.svds:
- if svd.version == 2 and svd.file_structure_version == 2:
- self.interchange_level = 4
- break
-
- extent_to_inode = {} # type: Dict[int, inode.Inode]
-
- # Parse all of the files starting from the PVD root directory record.
- ic_level, lastbyte = self._walk_directories(self.pvd, extent_to_ptr,
- extent_to_inode, le_ptrs)
-
- if self.eltorito_boot_catalog is not None:
- if not self.eltorito_boot_catalog.dirrecords:
- # We expect the boot catalog to have at *least* one directory
- # record attached. If we run across an ISO that doesn't have
- # that, we attach a "fake" one so that later steps do the right
- # thing. Note that this will never be written out since we
- # don't add it to the main PVD directory structure.
- new_record = dr.DirectoryRecord()
- new_record.new_file(self.pvd, self.logical_block_size,
- b'FAKEELT.;1',
- self.pvd.root_directory_record(), 0, '',
- b'', False, 0, time.time())
- self.eltorito_boot_catalog.add_dirrecord(new_record)
-
- self.interchange_level = max(self.interchange_level, ic_level)
-
- # After we have walked the directories we look to see if all of the
- # El Torito entries have corresponding directory records. If not, the
- # El Torito records may be 'hidden' or 'unlinked', meaning they have no
- # corresponding directory record in the ISO filesystem. In order to
- # accommodate the rest of the system which expects them to have
- # directory records, we use fake directory records that don't get
- # written out.
- #
- # Note that we specifically do *not* add these to any sort of parent;
- # that way, we don't run afoul of any checks that adding a child to a
- # parent might have. This means that if we do ever want to unhide this
- # entry, we'll have to do some additional work to give it a real name
- # and link it to the appropriate parent.
- if self.eltorito_boot_catalog is not None:
- self._link_eltorito(extent_to_inode)
-
- # Now that everything has a dirrecord, see if we have a boot
- # info table.
- self._check_for_eltorito_boot_info_table(self.eltorito_boot_catalog.initial_entry.inode)
- for sec in self.eltorito_boot_catalog.sections:
- for entry in sec.section_entries:
- self._check_for_eltorito_boot_info_table(entry.inode)
-
- # The PVD is finished. Now look to see if we need to parse the SVD.
- for svd in self.svds:
- if (svd.flags & 0x1) == 0 and svd.escape_sequences[:3] in (b'%/@', b'%/C', b'%/E'):
- if self.joliet_vd is not None:
- raise pycdlibexception.PyCdlibInvalidISO('Only a single Joliet SVD is supported')
-
- self.joliet_vd = svd
-
- le_ptrs, joliet_extent_to_ptr = self._parse_path_table(svd.path_table_size(),
- svd.path_table_location_le)
-
- tmp_be_ptrs, j_unused = self._parse_path_table(svd.path_table_size(),
- svd.path_table_location_be)
-
- for index, ptr in enumerate(le_ptrs):
- if not ptr.equal_to_be(tmp_be_ptrs[index]):
- raise pycdlibexception.PyCdlibInvalidISO('Joliet little-endian and big-endian path table records do not agree')
-
- self._walk_directories(svd, joliet_extent_to_ptr,
- extent_to_inode, le_ptrs)
- elif svd.version == 2 and svd.file_structure_version == 2:
- if self.enhanced_vd is not None:
- raise pycdlibexception.PyCdlibInvalidISO('Only a single enhanced VD is supported')
- self.enhanced_vd = svd
-
- # We've seen ISOs in the wild (Office XP) that have a PVD space size
- # that is smaller than the location of the last directory record
- # extent + length. If we see this, automatically update the size in the
- # PVD (and any SVDs) so that subsequent operations will be correct.
- if lastbyte > self.pvd.space_size * self.logical_block_size:
- new_pvd_size = utils.ceiling_div(lastbyte, self.logical_block_size)
- for pvd in self.pvds:
- pvd.space_size = new_pvd_size
- if self.joliet_vd is not None:
- self.joliet_vd.space_size = new_pvd_size
- if self.enhanced_vd is not None:
- self.enhanced_vd.space_size = new_pvd_size
-
- # Look to see if this is a UDF volume. It is one if we have a UDF BEA,
- # UDF NSR, and UDF TEA, in which case we parse the UDF descriptors and
- # walk the filesystem.
- if self._has_udf:
- self._parse_udf_descriptors()
- self._walk_udf_directories(extent_to_inode)
-
- # Now we look for the 'version' volume descriptor, common on ISOs made
- # with genisoimage or mkisofs. This volume descriptor doesn't have any
- # specification, but from code inspection, it is either a completely
- # zero extent, or starts with 'MKI'. Further, it starts directly after
- # the VDST, or directly after the UDF recognition sequence (if this is
- # a UDF ISO). Thus, we go looking for it at those places, and add it
- # if we find it there.
- version_vd_extent = self.vdsts[0].extent_location() + 1
- if self._has_udf:
- version_vd_extent = self.udf_teas[0].extent_location() + 1
-
- version_vd = headervd.VersionVolumeDescriptor()
- self._cdfp.seek(version_vd_extent * self.logical_block_size)
- if version_vd.parse(self._cdfp.read(self.logical_block_size), version_vd_extent):
- self.version_vd = version_vd
-
- self._initialized = True
-
- def _get_and_write_fp(self, iso_path, outfp, blocksize):
- # type: (bytes, BinaryIO, int) -> None
- """
- An internal method to fetch a single file from the ISO and write it out
- to the file object. The 'iso_path' argument can be a Joliet path, an
- ISO9660 path, or a Rock Ridge path. In the case that the same pathname
- exists in multiple contexts, the Joliet data will be tried first,
- followed by ISO9660, followed by Rock Ridge.
-
- Parameters:
- iso_path - The absolute path to the file to get data from.
- outfp - The file object to write data to.
- blocksize - The blocksize to use when copying data.
- Returns:
- Nothing.
- """
- if self.joliet_vd is not None:
- try:
- self._get_file_from_iso_fp(outfp, blocksize, None, None,
- iso_path)
- return
- except pycdlibexception.PyCdlibInvalidInput as err:
- if str(err) == 'Could not find path':
- pass
- else:
- raise
-
- saved_exception = None
- try:
- self._get_file_from_iso_fp(outfp, blocksize, iso_path, None, None)
- return
- except pycdlibexception.PyCdlibException as err:
- if str(err) == 'Could not find path':
- saved_exception = err
- else:
- raise
-
- if self.rock_ridge != '':
- self._get_file_from_iso_fp(outfp, blocksize, None, iso_path, None)
- return
-
- if saved_exception is not None:
- raise saved_exception
-
- def _udf_get_file_from_iso_fp(self, outfp, blocksize, udf_path):
- # type: (BinaryIO, int, bytes) -> None
- """
- An internal method to fetch a single UDF file from the ISO and write it
- out to the file object.
-
- Parameters:
- outfp - The file object to write data to.
- blocksize - The number of bytes in each transfer.
- udf_path - The absolute UDF path to lookup on the ISO.
- Returns:
- Nothing.
- """
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a udf_path from a non-UDF ISO')
-
- (ident_unused, found_file_entry) = self._find_udf_record(udf_path)
- if found_file_entry is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot get the contents of an empty UDF File Entry')
-
- if not found_file_entry.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Can only write out a file')
-
- if found_file_entry.inode is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot write out an entry without data')
-
- if found_file_entry.get_data_length() > 0:
- with inode.InodeOpenData(found_file_entry.inode, self.logical_block_size) as (data_fp, data_len):
- utils.copy_data(data_len, blocksize, data_fp, outfp)
-
- def _get_file_from_iso_fp(self, outfp, blocksize, iso_path, rr_path,
- joliet_path, encoding=''):
- # type: (BinaryIO, int, Optional[bytes], Optional[bytes], Optional[bytes], str) -> None
- """
- An internal method to fetch a single file from the ISO and write it out
- to the file object.
-
- Parameters:
- outfp - The file object to write data to.
- blocksize - The number of bytes in each transfer.
- iso_path - The absolute ISO9660 path to lookup on the ISO (exclusive
- with rr_path and joliet_path).
- rr_path - The absolute Rock Ridge path to lookup on the ISO (exclusive
- with iso_path and joliet_path).
- joliet_path - The absolute Joliet path to lookup on the ISO (exclusive
- with iso_path and rr_path).
- encoding - The string encoding used for the path.
- Returns:
- Nothing.
- """
- if joliet_path is not None:
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a joliet_path from a non-Joliet ISO')
- encoding = encoding or 'utf-16_be'
- found_record = self._find_joliet_record(joliet_path, encoding)
- elif rr_path is not None:
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a rr_path from a non-Rock Ridge ISO')
- encoding = encoding or 'utf-8'
- found_record = self._find_rr_record(rr_path, encoding)
- elif iso_path is not None:
- encoding = encoding or 'utf-8'
- found_record = self._find_iso_record(iso_path, encoding)
- else:
- raise pycdlibexception.PyCdlibInternalError('Invalid path passed to get_file_from_iso_fp')
-
- if found_record.is_dir():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot write out a directory')
-
- if rr_path is not None or iso_path is not None:
- if found_record.is_symlink():
- # If this Rock Ridge record is a symlink, it has no data
- # associated with it, so it makes no sense to try and get the
- # data. In theory, we could follow the symlink to the
- # appropriate place and get the data of the thing it points to.
- # However, Rock Ridge symlinks are allowed to point *outside*
- # of this ISO, so it is really not clear that this is something
- # we want to do. For now we make the user follow the symlink
- # themselves if they want to get the data. We can revisit this
- # decision in the future if we need to.
- raise pycdlibexception.PyCdlibInvalidInput('Symlinks have no data associated with them')
-
- if self.eltorito_boot_catalog is not None:
- for rec in self.eltorito_boot_catalog.dirrecords:
- if isinstance(rec, udfmod.UDFFileEntry):
- continue
- if rec.file_ident == found_record.file_ident and rec.parent == found_record.parent:
- recdata = self.eltorito_boot_catalog.record()
- outfp.write(recdata)
- utils.zero_pad(outfp, len(recdata), self.logical_block_size)
- return
-
- if found_record.inode is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot write out a file without data')
-
- while found_record.get_data_length() > 0:
- with inode.InodeOpenData(found_record.inode, self.logical_block_size) as (data_fp, data_len):
- # Copy the data into the output file descriptor. If a boot info
- # table is present, overlay the table over bytes 8-64 of the
- # file. Note that we never return more bytes than the length
- # of the file, so the boot info table may get truncated.
- if found_record.inode.boot_info_table is not None:
- header_len = min(data_len, 8)
- outfp.write(data_fp.read(header_len))
- data_len -= header_len
- if data_len > 0:
- bi_rec = found_record.inode.boot_info_table.record()
- table_len = min(data_len, len(bi_rec))
- outfp.write(bi_rec[:table_len])
- data_len -= table_len
- if data_len > 0:
- data_fp.seek(len(bi_rec), os.SEEK_CUR)
- utils.copy_data(data_len, blocksize, data_fp, outfp)
- else:
- utils.copy_data(data_len, blocksize, data_fp, outfp)
-
- if found_record.data_continuation is not None:
- found_record = found_record.data_continuation
- else:
- break
-
- class _WriteRange:
- """
- A class to store the offset and length of a written section of data.
- A sorted list of these is used to determine whether we are unintentionally
- spending time rewriting data that we have already written.
- """
- __slots__ = ('offset', 'length')
-
- def __init__(self, start, end):
- # type: (int, int) -> None
- self.offset = start
- self.length = start + (end - start)
-
- def __lt__(self, other):
- # When we go to insert this into the list, we determine if this one
- # overlaps with the one we are currently looking at.
- if range(max(other.offset, self.offset), min(other.length, self.length) + 1):
- raise pycdlibexception.PyCdlibInternalError('Overlapping write %s, %s' % (repr(self), repr(other)))
- return self.offset < other.offset
-
- def __repr__(self):
- return 'WriteRange: %s %s' % (self.offset, self.length)
-
- def _outfp_write_with_check(self, outfp, data, enable_overwrite_check=True):
- # type: (BinaryIO, bytes, bool) -> None
- """
- Internal method to write data out to the output file descriptor,
- ensuring that it doesn't go beyond the bounds of the ISO.
-
- Parameters:
- outfp - The file object to write to.
- data - The actual data to write.
- enable_overwrite_check - Whether to do overwrite checking if it is
- enabled. Some pieces of code explicitly want
- to overwrite data, so this allows them to
- disable the checking.
- Returns:
- Nothing.
- """
- start = outfp.tell()
- outfp.write(data)
- if self._track_writes:
- # After the write, double check that we didn't write beyond the
- # boundary of the PVD, and raise a PyCdlibException if we do.
- end = outfp.tell()
- if end > self.pvd.space_size * self.logical_block_size:
- raise pycdlibexception.PyCdlibInternalError('Wrote past the end of the ISO! (%d > %d)' % (end, self.pvd.space_size * self.logical_block_size))
-
- if enable_overwrite_check:
- bisect.insort_left(self._write_check_list, self._WriteRange(start, end - 1))
-
- def _output_file_data(self, outfp, blocksize, ino):
- # type: (BinaryIO, int, inode.Inode) -> Generator
- """
- Internal method to write a directory record entry out.
-
- Parameters:
- outfp - The file object to write the data to.
- blocksize - The blocksize to use when writing the data out.
- ino - The Inode to write.
- Returns:
- The total number of bytes written out.
- """
- outfp.seek(ino.extent_location() * self.logical_block_size)
- start_offset = outfp.tell()
- with inode.InodeOpenData(ino, self.logical_block_size) as (data_fp, data_len):
- for len_copied in utils.copy_data_yield(data_len, blocksize, data_fp, outfp): # pylint: disable=use-yield-from
- yield len_copied
- yield utils.zero_pad(outfp, data_len, self.logical_block_size)
-
- if self._track_writes:
- end = outfp.tell()
- bisect.insort_left(self._write_check_list,
- self._WriteRange(start_offset, end - 1))
-
- # If this file is being used as a bootfile, and a boot info table is
- # present, patch the boot info table into offset 8 here.
- if ino.boot_info_table is not None:
- old = outfp.tell()
- outfp.seek(start_offset + 8)
- rec = ino.boot_info_table.record()
- self._outfp_write_with_check(outfp, rec, enable_overwrite_check=False)
- outfp.seek(old)
-
- class _Progress:
- """
- An inner class to deal with progress.
- """
- __slots__ = ('done', 'total', 'progress_cb', 'progress_opaque', '_call')
-
- def __init__(self, total, progress_cb, progress_opaque):
- # type: (int, Optional[Callable[[int, int, Any], None]], Optional[Any]) -> None
- self.done = 0
- self.total = total
- self.progress_cb = progress_cb
- self.progress_opaque = progress_opaque
- if self.progress_cb is not None:
- arglen = len(inspect.getfullargspec(self.progress_cb).args)
-
- if arglen == 2:
- self._call = lambda done, total, opaque: self.progress_cb(done, total) # type: ignore
- elif arglen == 3:
- self._call = lambda done, total, opaque: self.progress_cb(done, total, opaque) # type: ignore # pylint: disable=unnecessary-lambda
- else:
- raise pycdlibexception.PyCdlibInvalidInput('The progress callback must take 2 or 3 arguments')
- else:
- self._call = lambda done, total, opaque: None
-
- def call(self, length):
- # type: (int) -> None
- """Add the length to done, then call progress_cb if it is not None."""
- self.done = min(self.done + length, self.total)
- self._call(self.done, self.total, self.progress_opaque)
-
- def finish(self):
- # type: () -> None
- """If the progress_cb is not None, call progress_cb with the final total."""
- # In almost all cases, this will cause self.done to wildly
- # overflow the total size. However, with the hard cap in
- # call, this works just fine.
- self.call(self.total)
-
- def _write_directory_records(self, vd, outfp, progress):
- # type: (headervd.PrimaryOrSupplementaryVD, BinaryIO, PyCdlib._Progress) -> None
- """
- An internal method to write out the directory records from a particular
- Volume Descriptor.
-
- Parameters:
- vd - The Volume Descriptor to write the Directory Records from.
- outfp - The file object to write data to.
- progress - The _Progress object to use for outputting progress.
- Returns:
- Nothing.
- """
- le_ptr_offset = 0
- be_ptr_offset = 0
- dirs = collections.deque([vd.root_directory_record()])
- while dirs:
- curr = dirs.popleft()
- curr_dirrecord_offset = 0
- if curr.is_dir():
- if curr.ptr is None:
- raise pycdlibexception.PyCdlibInternalError('Directory has no Path Table Record')
-
- # Little Endian PTR
- outfp.seek(vd.path_table_location_le * self.logical_block_size + le_ptr_offset)
- ret = curr.ptr.record_little_endian()
- self._outfp_write_with_check(outfp, ret)
- le_ptr_offset += len(ret)
-
- # Big Endian PTR
- outfp.seek(vd.path_table_location_be * self.logical_block_size + be_ptr_offset)
- ret = curr.ptr.record_big_endian()
- self._outfp_write_with_check(outfp, ret)
- be_ptr_offset += len(ret)
- progress.call(curr.get_data_length())
-
- dir_extent = curr.extent_location()
- for child in curr.children:
- # First write out the directory record entry for all children.
- recstr = child.record()
- if (curr_dirrecord_offset + len(recstr)) > self.logical_block_size:
- dir_extent += 1
- curr_dirrecord_offset = 0
- outfp.seek(dir_extent * self.logical_block_size + curr_dirrecord_offset)
- # Now write out the child.
- self._outfp_write_with_check(outfp, recstr)
- curr_dirrecord_offset += len(recstr)
-
- if child.rock_ridge is not None:
- if child.rock_ridge.dr_entries.ce_record is not None:
- # The child has a continue block, so write it out here.
- ce_rec = child.rock_ridge.dr_entries.ce_record
- outfp.seek(ce_rec.bl_cont_area * self.logical_block_size + ce_rec.offset_cont_area)
- rec = child.rock_ridge.record_ce_entries()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if child.rock_ridge.child_link_record_exists():
- continue
-
- if child.is_dir():
- # If the child is a directory, and is not dot or dotdot,
- # descend into it to look at the children.
- if not child.is_dot() and not child.is_dotdot():
- dirs.append(child)
-
- def _write_udf_descs(self, descs, outfp, progress):
- # type: (udfmod.UDFDescriptorSequence, BinaryIO, PyCdlib._Progress) -> None
- """
- An internal method to write out a UDF Descriptor sequence.
-
- Parameters:
- descs - The UDF Descriptors object to write out.
- outfp - The output file descriptor to use for writing.
- progress - The _Progress object to use for updating progress.
- Returns:
- Nothing.
- """
- for pvd in descs.pvds:
- outfp.seek(pvd.extent_location() * self.logical_block_size)
- rec = pvd.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if descs.desc_pointer.initialized:
- outfp.seek(descs.desc_pointer.extent_location() * self.logical_block_size)
- rec = descs.desc_pointer.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for impl_use in descs.impl_use:
- outfp.seek(impl_use.extent_location() * self.logical_block_size)
- rec = impl_use.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for partition in descs.partitions:
- outfp.seek(partition.extent_location() * self.logical_block_size)
- rec = partition.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for logical_volume in descs.logical_volumes:
- outfp.seek(logical_volume.extent_location() * self.logical_block_size)
- rec = logical_volume.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for unallocated_space in descs.unallocated_space:
- outfp.seek(unallocated_space.extent_location() * self.logical_block_size)
- rec = unallocated_space.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if descs.terminator.initialized:
- outfp.seek(descs.terminator.extent_location() * self.logical_block_size)
- rec = descs.terminator.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- def _write_fp(self, outfp, blocksize, progress_cb, progress_opaque):
- # type: (BinaryIO, int, Optional[Callable[[int, int, Any], None]], Optional[Any]) -> None
- """
- Write a properly formatted ISO out to the file object passed in. This
- also goes by the name of 'mastering'.
-
- Parameters:
- outfp - The file object to write the data to.
- blocksize - The blocksize to use when copying data.
- progress_cb - If not None, a function to call as the write call does its
- work. The callback function must have a signature of:
- def func(done, total, progress_data).
- progress_opaque - User data to be passed to the progress callback.
- Returns:
- Nothing.
- """
- if hasattr(outfp, 'mode') and 'b' not in outfp.mode:
- raise pycdlibexception.PyCdlibInvalidInput("The file to write out must be in binary mode (add 'b' to the open flags)")
-
- if self._needs_reshuffle:
- self._reshuffle_extents()
-
- self._write_check_list = []
- outfp.seek(0)
-
- progress = self._Progress(self.pvd.space_size * self.logical_block_size, progress_cb, progress_opaque)
- progress.call(0)
-
- if self.isohybrid_mbr is not None:
- self._outfp_write_with_check(outfp,
- self.isohybrid_mbr.record(self.pvd.space_size * self.logical_block_size))
-
- outfp.seek(self.pvd.extent_location() * self.logical_block_size)
-
- # First write out the PVDs.
- for pvd in self.pvds:
- rec = pvd.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Next write out the boot records.
- for br in self.brs:
- outfp.seek(br.extent_location() * self.logical_block_size)
- rec = br.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Next write out the SVDs.
- for svd in self.svds:
- outfp.seek(svd.extent_location() * self.logical_block_size)
- rec = svd.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Next write out the Volume Descriptor Terminators.
- for vdst in self.vdsts:
- outfp.seek(vdst.extent_location() * self.logical_block_size)
- rec = vdst.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Next write out the UDF Volume Recognition sequence (if this ISO has UDF).
- if self._has_udf:
- for bea in self.udf_beas:
- outfp.seek(bea.extent_location() * self.logical_block_size)
- rec = bea.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for boot in self.udf_boots:
- outfp.seek(boot.extent_location() * self.logical_block_size)
- rec = boot.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- outfp.seek(self.udf_nsr.extent_location() * self.logical_block_size)
- rec = self.udf_nsr.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- for tea in self.udf_teas:
- outfp.seek(tea.extent_location() * self.logical_block_size)
- rec = tea.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Next write out the version block if it exists.
- if self.version_vd is not None:
- outfp.seek(self.version_vd.extent_location() * self.logical_block_size)
- rec = self.version_vd.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if self._has_udf:
- # Now the UDF Main and Reserved Volume Descriptor Sequence.
- self._write_udf_descs(self.udf_main_descs, outfp, progress)
- self._write_udf_descs(self.udf_reserve_descs, outfp, progress)
-
- # Now the UDF Logical Volume Integrity Sequence (if there is one).
- if self.udf_logical_volume_integrity is not None:
- outfp.seek(self.udf_logical_volume_integrity.extent_location() * self.logical_block_size)
- rec = self.udf_logical_volume_integrity.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if self.udf_logical_volume_integrity_terminator is not None:
- outfp.seek(self.udf_logical_volume_integrity_terminator.extent_location() * self.logical_block_size)
- rec = self.udf_logical_volume_integrity_terminator.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Now the UDF Anchor Points (if there are any).
- for anchor in self.udf_anchors:
- outfp.seek(anchor.extent_location() * self.logical_block_size)
- rec = anchor.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # In theory, the Path Table Records (for both the PVD and SVD) get
- # written out next. Since we store them along with the Directory
- # Records, however, we will write them out along with the directory
- # records instead.
-
- # Now write out the El Torito Boot Catalog if it exists.
- if self.eltorito_boot_catalog is not None:
- outfp.seek(self.eltorito_boot_catalog.extent_location() * self.logical_block_size)
- rec = self.eltorito_boot_catalog.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- # Now write out the ISO9660 directory records.
- self._write_directory_records(self.pvd, outfp, progress)
-
- # Now write out the Joliet directory records, if they exist.
- if self.joliet_vd is not None:
- self._write_directory_records(self.joliet_vd, outfp, progress)
-
- # Now write out the UDF directory records, if they exist.
- if self.udf_root is not None:
- # Write out the UDF File Sets.
- outfp.seek(self.udf_file_set.extent_location() * self.logical_block_size)
- rec = self.udf_file_set.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- if self.udf_file_set_terminator is not None:
- outfp.seek(self.udf_file_set_terminator.extent_location() * self.logical_block_size)
- rec = self.udf_file_set_terminator.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
-
- written_file_entry_inodes = set()
- udf_file_entries = collections.deque([(self.udf_root, True)]) # type: Deque[Tuple[Optional[udfmod.UDFFileEntry], bool]]
- while udf_file_entries:
- udf_file_entry, isdir = udf_file_entries.popleft()
-
- if udf_file_entry is None:
- continue
-
- if udf_file_entry.inode is None or not id(udf_file_entry.inode) in written_file_entry_inodes:
- outfp.seek(udf_file_entry.extent_location() * self.logical_block_size)
- rec = udf_file_entry.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
- written_file_entry_inodes.add(id(udf_file_entry.inode))
-
- if isdir:
- outfp.seek(udf_file_entry.fi_descs[0].extent_location() * self.logical_block_size)
- # FIXME: for larger directories, we'll actually need to
- # iterate over the alloc_descs and write them
- for fi_desc in udf_file_entry.fi_descs:
- rec = fi_desc.record()
- self._outfp_write_with_check(outfp, rec)
- progress.call(len(rec))
- if not fi_desc.is_parent():
- udf_file_entries.append((fi_desc.file_entry, fi_desc.is_dir()))
-
- # Now write out the actual files. In many cases we haven't yet read the
- # file out of the original, so do that here.
- for ino in self.inodes:
- if ino.get_data_length() > 0:
- for len_copied in self._output_file_data(outfp, blocksize, ino):
- progress.call(len_copied)
-
- # Pad out to the total size of the disk, in case that the last thing
- # written is shorter than a full logical block size. Not all file-like
- # objects support truncate() to grow a file, so do it the old-fashioned
- # way by seeking to end - 1 and writing a padding '\x00' byte.
- outfp.seek(0, os.SEEK_END)
- total_size = self.pvd.space_size * self.logical_block_size
- if outfp.tell() != total_size:
- outfp.seek(total_size - 1)
- outfp.write(b'\x00')
-
- if self.isohybrid_mbr is not None:
- outfp.seek(0, 2)
- outfp.write(self.isohybrid_mbr.record_padding(self.pvd.space_size * self.logical_block_size))
- if self.isohybrid_mbr.efi:
- outfp.seek((self.isohybrid_mbr.secondary_gpt.header.current_lba * 512) - (self.isohybrid_mbr.secondary_gpt.header.num_parts * 128))
- outfp.write(self.isohybrid_mbr.secondary_gpt.record())
-
- progress.finish()
-
- def _update_rr_ce_entry(self, rec):
- # type: (dr.DirectoryRecord) -> int
- """
- An internal method to update the Rock Ridge CE entry for the given
- record.
-
- Parameters:
- rec - The record to update the Rock Ridge CE entry for (if it exists).
- Returns:
- The number of additional bytes needed for this Rock Ridge CE entry.
- """
- if rec.rock_ridge is not None and rec.rock_ridge.dr_entries.ce_record is not None:
- celen = rec.rock_ridge.dr_entries.ce_record.len_cont_area
- added_block, block, offset = self.pvd.add_rr_ce_entry(celen)
- rec.rock_ridge.update_ce_block(block)
- rec.rock_ridge.dr_entries.ce_record.update_offset(offset)
- if added_block:
- return self.logical_block_size
-
- return 0
-
- def _finish_add(self, num_bytes_to_add, num_partition_bytes_to_add):
- # type: (int, int) -> None
- """
- An internal method to do all of the accounting needed whenever
- something is added to the ISO. This method should only be called by
- public API methods.
-
- Parameters:
- num_bytes_to_add - The number of additional bytes to add to all
- descriptors.
- num_partition_bytes_to_add - The number of additional bytes to add to
- the partition if this is a UDF file.
- Returns:
- Nothing.
- """
- for pvd in self.pvds:
- pvd.add_to_space_size(num_bytes_to_add + num_partition_bytes_to_add)
- if self.joliet_vd is not None:
- self.joliet_vd.add_to_space_size(num_bytes_to_add + num_partition_bytes_to_add)
-
- if self.enhanced_vd is not None:
- self.enhanced_vd.copy_sizes(self.pvd)
-
- if self.udf_root is not None:
- num_extents_to_add = utils.ceiling_div(num_partition_bytes_to_add,
- self.logical_block_size)
-
- self.udf_main_descs.partitions[0].part_length += num_extents_to_add
- self.udf_reserve_descs.partitions[0].part_length += num_extents_to_add
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.size_tables[0] += num_extents_to_add
-
- if self._always_consistent:
- self._reshuffle_extents()
- else:
- self._needs_reshuffle = True
-
- def _finish_remove(self, num_bytes_to_remove, is_partition):
- # type: (int, bool) -> None
- """
- An internal method to do all of the accounting needed whenever
- something is removed from the ISO. This method should only be called
- by public API methods.
-
- Parameters:
- num_bytes_to_remove - The number of additional bytes to remove from the descriptors.
- is_partition - Whether these bytes are part of a UDF partition.
- Returns:
- Nothing.
- """
- for pvd in self.pvds:
- pvd.remove_from_space_size(num_bytes_to_remove)
- if self.joliet_vd is not None:
- self.joliet_vd.remove_from_space_size(num_bytes_to_remove)
-
- if self.enhanced_vd is not None:
- self.enhanced_vd.copy_sizes(self.pvd)
-
- if self.udf_root is not None and is_partition:
- num_extents_to_remove = utils.ceiling_div(num_bytes_to_remove,
- self.logical_block_size)
-
- self.udf_main_descs.partitions[0].part_length -= num_extents_to_remove
- self.udf_reserve_descs.partitions[0].part_length -= num_extents_to_remove
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.size_tables[0] -= num_extents_to_remove
-
- if self._always_consistent:
- self._reshuffle_extents()
- else:
- self._needs_reshuffle = True
-
- def _add_hard_link_to_inode(self, data_ino, length, file_mode,
- boot_catalog_old, **kwargs):
- # type: (Optional[inode.Inode], int, int, bool, Optional[str]) -> int
- """
- Add a hard link to the ISO. Hard links are alternate names for the
- same file contents that don't take up any additional space on the ISO.
- This API can be used to create hard links between two files on the
- ISO9660 filesystem, between two files on the Joliet filesystem, or
- between a file on the ISO9660 filesystem and the Joliet filesystem.
- In all cases, exactly one old path must be specified, and exactly one
- new path must be specified.
-
- Parameters:
- data_ino - The inode of the old record to link against.
- length - The length of the old record to link against.
- file_mode - The file mode of the old record to link against.
- boot_catalog_old - Whether this is a link to an old boot catalog.
- iso_new_path - The new path on the ISO9660 filesystem to link to.
- joliet_new_path - The new path on the Joliet filesystem to link to.
- rr_name - The Rock Ridge name to use for the new file if this is a
- Rock Ridge ISO and the new path is on the ISO9660 filesystem.
- udf_new_path - The new path on the UDF filesystem to link to.
- Returns:
- The number of bytes to add to the descriptors.
- """
- num_new = 0
- iso_new_path = None
- joliet_new_path = None
- rr_name = b''
- udf_new_path = None
- new_rec = None # type: Optional[Union[dr.DirectoryRecord, udfmod.UDFFileEntry]]
- for key, value in kwargs.items():
- if key == 'iso_new_path':
- if value is not None:
- num_new += 1
- iso_new_path = utils.normpath(value)
- if not self.rock_ridge and self.interchange_level < 4:
- _check_path_depth(iso_new_path)
- elif key == 'joliet_new_path':
- if value is not None:
- num_new += 1
- joliet_new_path = self._normalize_joliet_path(value)
- elif key == 'rr_name':
- if value is not None:
- rr_name = self._check_rr_name(value)
- elif key == 'udf_new_path':
- if value is not None:
- num_new += 1
- udf_new_path = utils.normpath(value)
- else:
- raise pycdlibexception.PyCdlibInvalidInput('Unknown keyword %s' % (key))
-
- if num_new != 1:
- raise pycdlibexception.PyCdlibInvalidInput('Exactly one new path must be specified')
- if self.rock_ridge and iso_new_path is not None and not rr_name:
- raise pycdlibexception.PyCdlibInvalidInput('Rock Ridge name must be supplied for a Rock Ridge new path')
-
- num_bytes_to_add = 0
- if udf_new_path is None:
- if iso_new_path is not None:
- # ... to another file on the ISO9660 filesystem.
- (new_name, new_parent) = self._iso_name_and_parent_from_path(iso_new_path)
- _check_iso9660_filename(new_name, self.interchange_level)
- vd = self.pvd
- rr = self.rock_ridge
- xa = self.xa
- else:
- # Above we checked to make sure we got exactly one new path, so
- # we know for certain that this is Joliet.
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to link to Joliet record on non-Joliet ISO')
- if joliet_new_path is None:
- # This can really never happen, but here to make mypy happy
- raise pycdlibexception.PyCdlibInternalError('Invalid joliet_new_path')
- # ... to a file on the Joliet filesystem.
- (new_name, new_parent) = self._joliet_name_and_parent_from_path(joliet_new_path)
- vd = self.joliet_vd
- rr = ''
- xa = False
-
- new_rec = dr.DirectoryRecord()
- new_rec.new_file(vd, length, new_name, new_parent,
- vd.sequence_number(), rr, rr_name, xa, file_mode,
- time.time())
-
- num_bytes_to_add += self._add_child_to_dr(new_rec)
- num_bytes_to_add += self._update_rr_ce_entry(new_rec)
- else:
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
-
- # UDF new path.
- (udf_name, udf_parent) = self._udf_name_and_parent_from_path(udf_new_path)
-
- file_ident = udfmod.UDFFileIdentifierDescriptor()
- file_ident.new(False, False, udf_name, udf_parent)
- num_new_extents = udf_parent.add_file_ident_desc(file_ident,
- self.logical_block_size)
- num_bytes_to_add += num_new_extents * self.logical_block_size
-
- file_entry = udfmod.UDFFileEntry()
- file_entry.new(length, 'file', udf_parent, self.logical_block_size)
- file_ident.file_entry = file_entry
- file_entry.file_ident = file_ident
- if data_ino is None or data_ino.num_udf == 0:
- num_bytes_to_add += self.logical_block_size
-
- if data_ino is not None:
- data_ino.num_udf += 1
-
- new_rec = file_entry
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_impl_use.num_files += 1
-
- if data_ino is not None and new_rec is not None:
- data_ino.linked_records.append((new_rec, iso_new_path is not None))
- new_rec.inode = data_ino
-
- if boot_catalog_old and new_rec is not None:
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to link to El Torito on non-El Torito ISO')
- self.eltorito_boot_catalog.add_dirrecord(new_rec)
-
- return num_bytes_to_add
-
- def _add_fp(self, fp, length, manage_fp, iso_path, rr_name,
- joliet_path, udf_path, file_mode, eltorito_catalog):
- # type: (Optional[Union[BinaryIO, str]], int, bool, Optional[str], Optional[str], Optional[str], Optional[str], Optional[int], bool) -> int
- """
- An internal method to add a file to the ISO. If the ISO contains Rock
- Ridge, then a Rock Ridge name must be provided. If the ISO contains
- Joliet, then a Joliet path is not required but is highly recommended.
- Note that the caller must ensure that the file remains open for the
- lifetime of the ISO object, as the PyCdlib class uses the file
- descriptor internally when writing (mastering) the ISO.
-
- Parameters:
- fp - The file object to use for the contents of the new file.
- length - The length of the data for the new file.
- manage_fp - Whether or not pycdlib should internally manage the file
- pointer. It is faster to manage the file pointer
- externally, but it is more convenient to have pycdlib do it
- internally.
- iso_path - The ISO9660 absolute path to the file destination on the ISO.
- rr_name - The Rock Ridge name of the file destination on the ISO.
- joliet_path - The Joliet absolute path to the file destination on the ISO.
- udf_path - The UDF absolute path to the file destination on the ISO.
- file_mode - The POSIX file_mode to apply to this file. This only
- applies if this is a Rock Ridge ISO. If this is None (the
- default), the permissions from the original file are used.
- eltorito_catalog - Whether this entry represents an El Torito Boot
- Catalog.
- Returns:
- The number of bytes to add to the descriptors.
- """
-
- if iso_path is None and joliet_path is None and udf_path is None:
- raise pycdlibexception.PyCdlibInvalidInput("At least one of 'iso_path', 'joliet_path', or 'udf_path' must be provided")
-
- fmode = 0
- if file_mode is not None:
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a file mode for Rock Ridge ISOs')
- fmode = file_mode
- else:
- if self.rock_ridge:
- if fp is not None and not isinstance(fp, str):
- # Python 3 implements the fileno method for all file-like
- # objects, so we can't just use the existence of the method
- # to tell whether it is available. Instead, we try to
- # assign it, and if that fails, then we assume it is not
- # available.
- try:
- fileno = fp.fileno()
- fmode = os.fstat(fileno).st_mode
- except (AttributeError, io.UnsupportedOperation):
- # We couldn't get the actual file mode of the file, so
- # assume a conservative 444
- fmode = 0o0100444
- else:
- fmode = 0o0100444
-
- if length > (2**32) - 1 and self.interchange_level < 3:
- raise pycdlibexception.PyCdlibInvalidInput('File sizes for interchange level < 3 must be less than 4GiB')
-
- left = length
- offset = 0
- done = False
- num_bytes_to_add = 0
- while not done:
- # The maximum length we allow in one directory record is 0xfffff800
- # (this is taken from xorriso, though I don't really know why).
- thislen = min(left, 0xfffff800)
-
- ino = None
- if fp is not None:
- ino = inode.Inode()
- ino.new(thislen, fp, manage_fp, offset)
-
- num_bytes_to_add += thislen
- if iso_path:
- num_bytes_to_add += self._add_hard_link_to_inode(ino, thislen,
- fmode,
- eltorito_catalog,
- iso_new_path=iso_path,
- rr_name=rr_name)
-
- if joliet_path:
- # If this is a Joliet ISO, then we can re-use add_hard_link to do
- # most of the work.
- num_bytes_to_add += self._add_hard_link_to_inode(ino, thislen,
- fmode,
- eltorito_catalog,
- joliet_new_path=joliet_path)
-
- # This goes after the hard link so we only track the new Inode if
- # everything above succeeds
- if ino is not None:
- self.inodes.append(ino)
-
- left -= thislen
- offset += thislen
- if left == 0:
- done = True
-
- if udf_path:
- num_bytes_to_add += self._add_hard_link_to_inode(ino, length,
- fmode,
- eltorito_catalog,
- udf_new_path=udf_path)
-
- return num_bytes_to_add
-
- def _rm_dr_link(self, rec):
- # type: (dr.DirectoryRecord) -> int
- """
- An internal method to remove a Directory Record link given the record.
-
- Parameters:
- rec - The Directory Record to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
- if not rec.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove a directory with rm_hard_link (try rm_directory instead)')
-
- num_bytes_to_remove = 0
-
- done = False
- while not done:
- num_bytes_to_remove += self._remove_child_from_dr(rec,
- rec.index_in_parent)
-
- if rec.inode is not None:
- found_index = None
- for index, reclink in enumerate(rec.inode.linked_records):
- link = reclink[0]
- if id(link) == id(rec):
- found_index = index
- break
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Could not find inode corresponding to record')
-
- del rec.inode.linked_records[found_index]
-
- # We only remove the size of the child from the ISO if there are
- # no other references to this file on the ISO.
- if not rec.inode.linked_records:
- found_index = None
- for index, ino in enumerate(self.inodes):
- if id(ino) == id(rec.inode):
- found_index = index
- break
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Could not find inode corresponding to record')
- del self.inodes[found_index]
-
- num_bytes_to_remove += rec.get_data_length()
-
- if rec.data_continuation is not None:
- rec = rec.data_continuation
- else:
- done = True
-
- return num_bytes_to_remove
-
- def _rm_udf_file_ident(self, parent, fi):
- # type: (udfmod.UDFFileEntry, bytes) -> int
- """
- An internal method to remove a UDF File Identifier from the parent
- and remove any space from the Logical Volume as necessary.
-
- Parameters:
- parent - The parent entry to remove the UDF File Identifier from.
- fi - The file identifier to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
- num_extents_to_remove = parent.remove_file_ident_desc_by_name(fi,
- self.logical_block_size)
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_impl_use.num_files -= 1
-
- self._find_udf_record.cache_clear() # pylint: disable=no-member
-
- return num_extents_to_remove * self.logical_block_size
-
- def _rm_udf_link(self, rec):
- # type: (udfmod.UDFFileEntry) -> int
- """
- An internal method to remove a UDF File Entry link.
-
- Parameters:
- rec - The UDF File Entry to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
- if not rec.is_file() and not rec.is_symlink():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove a directory with rm_hard_link (try rm_directory instead)')
-
- # To remove something from UDF, we have to:
- # 1. Remove it from the list of linked_records on the Inode.
- # 2. If the number of links to the Inode is now 0, remove the Inode.
- # 3. If the number of links to the UDF File Entry this uses is 0,
- # remove the UDF File Entry.
- # 4. Remove the UDF File Identifier from the parent.
-
- num_bytes_to_remove = 0
-
- if rec.inode is not None:
- # Step 1.
- found_index = None
- for index, reclink in enumerate(rec.inode.linked_records):
- link = reclink[0]
- if id(link) == id(rec):
- found_index = index
- break
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Could not find inode corresponding to record')
-
- del rec.inode.linked_records[found_index]
- rec.inode.num_udf -= 1
-
- # Step 2.
- if not rec.inode.linked_records:
- found_index = None
- for index, ino in enumerate(self.inodes):
- if id(ino) == id(rec.inode):
- found_index = index
- break
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Could not find inode corresponding to record')
- del self.inodes[found_index]
-
- num_bytes_to_remove += rec.get_data_length()
-
- # Step 3.
- if rec.inode.num_udf == 0:
- num_bytes_to_remove += self.logical_block_size
- else:
- # If rec.inode is None, just remove space for the UDF File Entry.
- num_bytes_to_remove += self.logical_block_size
-
- # Step 4.
- if rec.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Cannot remove a UDF record with no parent')
- if rec.file_ident is None:
- raise pycdlibexception.PyCdlibInternalError('Cannot remove a UDF record with no file identifier')
- return num_bytes_to_remove + self._rm_udf_file_ident(rec.parent, rec.file_ident.fi)
-
- def _add_joliet_dir(self, joliet_path):
- # type: (bytes) -> int
- """
- An internal method to add a joliet directory to the ISO.
-
- Parameters:
- joliet_path - The path to add to the Joliet portion of the ISO.
- Returns:
- The number of additional bytes needed on the ISO to fit this directory.
- """
-
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to add Joliet dir to non-Joliet ISO')
-
- (joliet_name, joliet_parent) = self._joliet_name_and_parent_from_path(joliet_path)
-
- rec = dr.DirectoryRecord()
- rec.new_dir(self.joliet_vd, joliet_name, joliet_parent,
- self.joliet_vd.sequence_number(), '', b'',
- self.logical_block_size, False, False,
- False, -1, time.time())
- num_bytes_to_add = self._add_child_to_dr(rec)
-
- self._create_dot(self.joliet_vd, rec, '', False, -1)
- self._create_dotdot(self.joliet_vd, rec, '', False, False, -1)
-
- num_bytes_to_add += self.logical_block_size
- if self.joliet_vd.add_to_ptr_size(path_table_record.PathTableRecord.record_length(len(joliet_name))):
- num_bytes_to_add += 4 * self.logical_block_size
-
- ptr = path_table_record.PathTableRecord()
- ptr.new_dir(joliet_name)
- rec.set_ptr(ptr)
-
- return num_bytes_to_add
-
- def _rm_joliet_dir(self, joliet_path):
- # type: (bytes) -> int
- """
- An internal method to remove a directory from the Joliet portion of the ISO.
-
- Parameters:
- joliet_path - The Joliet directory to remove.
- Returns:
- The number of bytes to remove from the ISO for this Joliet directory.
- """
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to remove joliet dir from non-Joliet ISO')
-
- joliet_child = self._find_joliet_record(joliet_path)
- num_bytes_to_remove = joliet_child.get_data_length()
- num_bytes_to_remove += self._remove_child_from_dr(joliet_child,
- joliet_child.index_in_parent)
-
- if joliet_child.ptr is None:
- raise pycdlibexception.PyCdlibInternalError('Joliet directory has no path table record; this should not be')
- if self.joliet_vd.remove_from_ptr_size(path_table_record.PathTableRecord.record_length(joliet_child.ptr.len_di)):
- num_bytes_to_remove += 4 * self.logical_block_size
-
- return num_bytes_to_remove
-
- def _get_iso_entry(self, iso_path, encoding='utf-8'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- Internal method to get the directory record for an ISO path.
-
- Parameters:
- iso_path - The path on the ISO filesystem to look up the record for.
- encoding - The string encoding used for the path.
- Returns:
- A dr.DirectoryRecord object representing the path.
- """
- if self._needs_reshuffle:
- self._reshuffle_extents()
-
- return self._find_iso_record(iso_path, encoding)
-
- def _get_rr_entry(self, rr_path, encoding='utf-8'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- Internal method to get the directory record for a Rock Ridge path.
-
- Parameters:
- rr_path - The Rock Ridge path on the ISO filesystem to look up the
- record for.
- encoding - The string encoding used for the path.
- Returns:
- A dr.DirectoryRecord object representing the path.
- """
- if self._needs_reshuffle:
- self._reshuffle_extents()
-
- return self._find_rr_record(rr_path, encoding)
-
- def _get_joliet_entry(self, joliet_path, encoding='utf-16_be'):
- # type: (bytes, str) -> dr.DirectoryRecord
- """
- Internal method to get the directory record for a Joliet path.
-
- Parameters:
- joliet_path - The path on the Joliet filesystem to look up the record
- for.
- encoding - The string encoding used for the path.
- Returns:
- A dr.DirectoryRecord object representing the path.
- """
- if self._needs_reshuffle:
- self._reshuffle_extents()
-
- return self._find_joliet_record(joliet_path, encoding)
-
- def _get_udf_entry(self, udf_path):
- # type: (str) -> udfmod.UDFFileEntry
- """
- Internal method to get the UDF File Entry for a particular path.
-
- Parameters:
- udf_path - The path on the UDF filesystem to look up the record for.
- Returns:
- A udfmod.UDFFileEntry object representing the path.
- """
- if self._needs_reshuffle:
- self._reshuffle_extents()
-
- return self._find_udf_record(utils.normpath(udf_path))[1]
-
- def _check_inode_against_eltorito(self, ino):
- # type: (inode.Inode) -> None
- """
- Internal method to check if an Inode is referenced by El Torito at all.
-
- Parameters:
- ino - The Inode to look for in the El Torito part of the ISO.
- Returns:
- Nothing.
- """
- if self.eltorito_boot_catalog is not None:
- eltorito_entries = set()
- eltorito_entries.add(id(self.eltorito_boot_catalog.initial_entry.inode))
- for sec in self.eltorito_boot_catalog.sections:
- for entry in sec.section_entries:
- eltorito_entries.add(id(entry.inode))
-
- if id(ino) in eltorito_entries:
- raise pycdlibexception.PyCdlibInvalidInput("Cannot remove a file that is referenced by El Torito; use 'rm_eltorito' to remove El Torito, or use 'rm_hard_link' to hide the entry")
-
- def _rm_file_inodes(self, child):
- # type: (dr.DirectoryRecord) -> int
- """
- Internal method to remove all of the Directory Records and UDF File
- Entries linked to this child's Inode.
-
- Parameters:
- child - The DirectoryRecord to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
-
- if not child.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove a directory with rm_file (try rm_directory instead)')
-
- # We also want to check to see if this Directory Record is currently
- # being used as an El Torito Boot Catalog, Initial Entry, or Section
- # Entry. If it is, we throw an exception; we don't know if the user
- # meant to remove El Torito from this ISO, or if they meant to 'hide'
- # the entry, but we need them to call the correct API to let us know.
- if self.eltorito_boot_catalog is not None:
- if any(id(child) == id(rec) for rec in self.eltorito_boot_catalog.dirrecords):
- raise pycdlibexception.PyCdlibInvalidInput("Cannot remove a file that is referenced by El Torito; use 'rm_eltorito' to remove El Torito, or use 'rm_hard_link' to hide the entry")
-
- num_bytes_to_remove = 0
-
- if child.inode is None:
- num_bytes_to_remove += self._remove_child_from_dr(child,
- child.index_in_parent)
- else:
- self._check_inode_against_eltorito(child.inode)
- while child.inode.linked_records:
- rec = child.inode.linked_records[0][0]
-
- if isinstance(rec, dr.DirectoryRecord):
- num_bytes_to_remove += self._rm_dr_link(rec)
- elif isinstance(rec, udfmod.UDFFileEntry):
- num_bytes_to_remove += self._rm_udf_link(rec)
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Saw a linked record that was neither ISO or UDF')
-
- return num_bytes_to_remove
-
- def _rm_file_via_iso_path(self, iso_path):
- # type: (str) -> int
- """
- Internal method to completely remove a file given an ISO path.
-
- Parameters:
- iso_path - The path to the file in the ISO9660 context to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
-
- iso_path_bytes = utils.normpath(iso_path)
-
- return self._rm_file_inodes(self._find_iso_record(iso_path_bytes))
-
- def _rm_file_via_joliet_path(self, joliet_path):
- # type: (str) -> int
- """
- Internal method to completely remove a file given a Joliet path.
-
- Parameters:
- joliet_path - The path to the file in the Joliet context to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
-
- joliet_path_bytes = self._normalize_joliet_path(joliet_path)
-
- return self._rm_file_inodes(self._find_joliet_record(joliet_path_bytes))
-
- def _rm_file_via_udf_path(self, udf_path):
- # type: (str) -> int
- """
- Internal method to completely remove a file given a UDF path.
-
- Parameters:
- udf_path - The path to the file in the UDF context to remove.
- Returns:
- The number of bytes to remove from the ISO.
- """
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
-
- udf_path_bytes = utils.normpath(udf_path)
- (udf_file_ident, udf_file_entry) = self._find_udf_record(udf_path_bytes)
-
- num_bytes_to_remove = 0
- if udf_file_entry is None:
- if udf_file_ident is not None and udf_file_ident.parent is not None:
- # If the udf_path was specified, go looking for the UDF File Ident
- # that corresponds to this record. If the UDF File Ident exists,
- # and the File Entry is None, this means that it is an "zeroed"
- # UDF File Entry and we have to remove it by hand.
- self._rm_udf_file_ident(udf_file_ident.parent, udf_file_ident.fi)
- # We also have to remove the "zero" UDF File Entry, since nothing
- # else will.
- num_bytes_to_remove += self.logical_block_size
- else:
- if not udf_file_entry.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove a directory with rm_file (try rm_directory instead)')
-
- # We also want to check to see if this Directory Record is currently
- # being used as an El Torito Boot Catalog, Initial Entry, or Section
- # Entry. If it is, we throw an exception; we don't know if the user
- # meant to remove El Torito from this ISO, or if they meant to 'hide'
- # the entry, but we need them to call the correct API to let us know.
- if self.eltorito_boot_catalog is not None:
- if any(id(udf_file_entry) == id(rec) for rec in self.eltorito_boot_catalog.dirrecords):
- raise pycdlibexception.PyCdlibInvalidInput("Cannot remove a file that is referenced by El Torito; use 'rm_eltorito' to remove El Torito, or use 'rm_hard_link' to hide the entry")
-
- if udf_file_entry.inode is not None:
- self._check_inode_against_eltorito(udf_file_entry.inode)
-
- while udf_file_entry.inode.linked_records:
- rec = udf_file_entry.inode.linked_records[0][0]
-
- if isinstance(rec, dr.DirectoryRecord):
- num_bytes_to_remove += self._rm_dr_link(rec)
- elif isinstance(rec, udfmod.UDFFileEntry):
- num_bytes_to_remove += self._rm_udf_link(rec)
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Saw a linked record that was neither ISO or UDF')
-
- return num_bytes_to_remove
-
- def _create_dot(self, vd, parent, rock_ridge, xa, file_mode):
- # type: (headervd.PrimaryOrSupplementaryVD, dr.DirectoryRecord, str, bool, int) -> None
- """
- An internal method to create a new 'dot' Directory Record.
-
- Parameters:
- vd - The volume descriptor to attach the 'dot' Directory Record to.
- parent - The parent Directory Record for new Directory Record.
- rock_ridge - The Rock Ridge version to use for this entry (if any).
- xa - Whether this Directory Record should have extended attributes.
- file_mode - The mode to assign to the dot directory (only applies to Rock Ridge).
- Returns:
- Nothing.
- """
- dot = dr.DirectoryRecord()
- dot.new_dot(vd, parent, vd.sequence_number(), rock_ridge,
- vd.logical_block_size(), xa, file_mode, time.time())
- self._add_child_to_dr(dot)
-
- def _create_dotdot(self, vd, parent, rock_ridge, relocated, xa, file_mode):
- # type: (headervd.PrimaryOrSupplementaryVD, dr.DirectoryRecord, str, bool, bool, int) -> dr.DirectoryRecord
- """
- An internal method to create a new 'dotdot' Directory Record.
-
- Parameters:
- vd - The volume descriptor to attach the 'dotdot' Directory Record to.
- parent - The parent Directory Record for new Directory Record.
- rock_ridge - The Rock Ridge version to use for this entry (if any).
- relocated - Whether this Directory Record is a Rock Ridge relocated entry.
- xa - Whether this Directory Record should have extended attributes.
- file_mode - The mode to assign to the dot directory (only applies to Rock Ridge).
- Returns:
- Nothing.
- """
- dotdot = dr.DirectoryRecord()
- dotdot.new_dotdot(vd, parent, vd.sequence_number(), rock_ridge,
- vd.logical_block_size(), relocated, xa, file_mode,
- time.time())
- self._add_child_to_dr(dotdot)
- return dotdot
-
- ########################### PUBLIC API #####################################
-
- def __init__(self, always_consistent=False):
- # type: (bool) -> None
- self._always_consistent = always_consistent
- track_writes = os.getenv('PYCDLIB_TRACK_WRITES')
- self._track_writes = False
- if track_writes is not None:
- self._track_writes = True
- self._initialize()
-
- def new(self, interchange_level=1, sys_ident='', vol_ident='', set_size=1,
- seqnum=1, log_block_size=2048, vol_set_ident=' ', pub_ident_str='',
- preparer_ident_str='', app_ident_str='', copyright_file='',
- abstract_file='', bibli_file='', vol_expire_date=None, app_use='',
- joliet=None, rock_ridge=None, xa=False, udf=None):
- # type: (int, str, str, int, int, int, str, str, str, str, str, str, str, Optional[float], str, Optional[int], Optional[str], bool, Optional[str]) -> None
- """
- Create a new ISO from scratch.
-
- Parameters:
- interchange_level - The ISO9660 interchange level to use; this dictates
- the rules on the names of files. Levels 1, 2, 3,
- and 4 are supported. Level 1 is the most
- conservative, and is the default, but level 3 is
- recommended.
- sys_ident - The system identification string to use on the new ISO.
- vol_ident - The volume identification string to use on the new ISO.
- set_size - The size of the set of ISOs this ISO is a part of.
- seqnum - The sequence number of the set of this ISO.
- log_block_size - The logical block size to use for the ISO. While ISO9660
- technically supports sizes other than 2048 (the default),
- this almost certainly doesn't work.
- vol_set_ident - The volume set identification string to use on the new ISO.
- pub_ident_str - The publisher identification string to use on the new ISO.
- preparer_ident_str - The preparer identification string to use on the new ISO.
- app_ident_str - The application identification string to use on the new ISO.
- copyright_file - The name of a file at the root of the ISO to use as the
- copyright file.
- abstract_file - The name of a file at the root of the ISO to use as the
- abstract file.
- bibli_file - The name of a file at the root of the ISO to use as the
- bibliographic file.
- vol_expire_date - The date that this ISO will expire at.
- app_use - Arbitrary data that the application can stuff into the primary
- volume descriptor of this ISO.
- joliet - A integer that can have the value 1, 2, or 3 for Joliet
- levels 1, 2, or 3 (3 is by far the most common), or None for
- no Joliet support (the default). For legacy reasons, this
- parameter also accepts a boolean, where the value of 'False'
- means no Joliet and a value of 'True' means level 3.
- rock_ridge - Whether to make this ISO have the Rock Ridge extensions or
- not. The default value of None does not add Rock Ridge
- extensions. A string value of '1.09', '1.10', or '1.12'
- adds the specified Rock Ridge version to the ISO. If
- unsure, pass '1.09' to ensure maximum compatibility.
- xa - Whether to add the ISO9660 Extended Attribute extensions to this
- ISO. The default is False.
- udf - Whether to add UDF support to this ISO. If it is None (the
- default), no UDF support is added. If it is "2.60", version 2.60
- of the UDF spec is used. All other values are disallowed.
- Returns:
- Nothing.
- """
- if self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object already has an ISO; either close it or create a new object')
-
- if interchange_level < 1 or interchange_level > 4:
- raise pycdlibexception.PyCdlibInvalidInput('Invalid interchange level (must be between 1 and 4)')
-
- if rock_ridge and rock_ridge not in ('1.09', '1.10', '1.12'):
- raise pycdlibexception.PyCdlibInvalidInput('Rock Ridge value must be None (no Rock Ridge), 1.09, 1.10, or 1.12')
-
- if udf and udf != '2.60':
- raise pycdlibexception.PyCdlibInvalidInput('UDF value must be empty (no UDF), or 2.60')
-
- if not app_ident_str:
- app_ident_str = 'PyCdlib (C) 2015-2020 Chris Lalancette'
-
- self.interchange_level = interchange_level
-
- self.xa = xa
-
- if isinstance(joliet, bool):
- if joliet:
- joliet = 3
- else:
- joliet = None
-
- if rock_ridge:
- self.rock_ridge = rock_ridge
-
- sys_ident_bytes = sys_ident.encode('utf-8')
- vol_ident_bytes = vol_ident.encode('utf-8')
- vol_set_ident_bytes = vol_set_ident.encode('utf-8')
- pub_ident_bytes = pub_ident_str.encode('utf-8')
- preparer_ident_bytes = preparer_ident_str.encode('utf-8')
- app_ident_bytes = app_ident_str.encode('utf-8')
- copyright_file_bytes = copyright_file.encode('utf-8')
- abstract_file_bytes = abstract_file.encode('utf-8')
- bibli_file_bytes = bibli_file.encode('utf-8')
- app_use_bytes = app_use.encode('utf-8')
-
- if vol_expire_date is None:
- real_vol_expire_date = 0.0
- else:
- real_vol_expire_date = vol_expire_date
-
- # Now start creating the ISO.
- self.pvd = headervd.pvd_factory(sys_ident_bytes, vol_ident_bytes,
- set_size, seqnum, log_block_size,
- vol_set_ident_bytes, pub_ident_bytes,
- preparer_ident_bytes, app_ident_bytes,
- copyright_file_bytes,
- abstract_file_bytes, bibli_file_bytes,
- real_vol_expire_date, app_use_bytes, xa)
- self.pvds.append(self.pvd)
-
- self.logical_block_size = self.pvd.logical_block_size()
-
- num_bytes_to_add = 0
- if self.interchange_level == 4:
- self.enhanced_vd = headervd.enhanced_vd_factory(sys_ident_bytes,
- vol_ident_bytes,
- set_size, seqnum,
- log_block_size,
- vol_set_ident_bytes,
- pub_ident_bytes,
- preparer_ident_bytes,
- app_ident_bytes,
- copyright_file_bytes,
- abstract_file_bytes,
- bibli_file_bytes,
- real_vol_expire_date,
- app_use_bytes, xa)
- self.svds.append(self.enhanced_vd)
-
- num_bytes_to_add += self.enhanced_vd.logical_block_size()
-
- if joliet is not None:
- self.joliet_vd = headervd.joliet_vd_factory(joliet, sys_ident_bytes,
- vol_ident_bytes, set_size,
- seqnum, log_block_size,
- vol_set_ident_bytes,
- pub_ident_bytes,
- preparer_ident_bytes,
- app_ident_bytes,
- copyright_file_bytes,
- abstract_file_bytes,
- bibli_file_bytes,
- real_vol_expire_date,
- app_use_bytes, xa)
- self.svds.append(self.joliet_vd)
-
- # Now that we have added joliet, we need to add the new space to the
- # PVD for the VD itself.
- num_bytes_to_add += self.joliet_vd.logical_block_size()
-
- self.vdsts.append(headervd.vdst_factory())
- num_bytes_to_add += self.logical_block_size
-
- if udf:
- self._has_udf = True
- # Create the UDF Bridge Recognition Volume Sequence.
- udf_bea = udfmod.BEAVolumeStructure()
- udf_bea.new()
- self.udf_beas.append(udf_bea)
-
- self.udf_nsr.new(2)
-
- udf_tea = udfmod.TEAVolumeStructure()
- udf_tea.new()
- self.udf_teas.append(udf_tea)
-
- num_bytes_to_add += 3 * self.logical_block_size
-
- # We always create an empty version volume descriptor.
- self.version_vd = headervd.version_vd_factory(self.logical_block_size)
- num_bytes_to_add += self.logical_block_size
-
- if udf:
- # We need to pad out to extent 32. The padding should be the
- # distance between the current PVD space size and 32.
- additional_extents = 32 - (self.pvd.space_size + (num_bytes_to_add // self.logical_block_size))
- num_bytes_to_add += additional_extents * self.logical_block_size
-
- # Create the Main Volume Descriptor Sequence.
- pvd = udfmod.UDFPrimaryVolumeDescriptor()
- pvd.new()
- self.udf_main_descs.pvds.append(pvd)
-
- impl_use = udfmod.UDFImplementationUseVolumeDescriptor()
- impl_use.new()
- self.udf_main_descs.impl_use.append(impl_use)
-
- partition = udfmod.UDFPartitionVolumeDescriptor()
- partition.new(2) # FIXME: we should let the user set this
- self.udf_main_descs.partitions.append(partition)
-
- logical_volume = udfmod.UDFLogicalVolumeDescriptor()
- logical_volume.new()
- logical_volume.add_partition_map(1)
- self.udf_main_descs.logical_volumes.append(logical_volume)
-
- unallocated_space = udfmod.UDFUnallocatedSpaceDescriptor()
- unallocated_space.new()
- self.udf_main_descs.unallocated_space.append(unallocated_space)
-
- self.udf_main_descs.terminator.new()
-
- num_bytes_to_add += 16 * self.logical_block_size
-
- # Create the Reserve Volume Descriptor Sequence.
- reserve_pvd = udfmod.UDFPrimaryVolumeDescriptor()
- reserve_pvd.new()
- self.udf_reserve_descs.pvds.append(reserve_pvd)
-
- reserve_impl_use = udfmod.UDFImplementationUseVolumeDescriptor()
- reserve_impl_use.new()
- self.udf_reserve_descs.impl_use.append(reserve_impl_use)
-
- reserve_partition = udfmod.UDFPartitionVolumeDescriptor()
- reserve_partition.new(2)
- self.udf_reserve_descs.partitions.append(reserve_partition)
-
- reserve_logical_volume = udfmod.UDFLogicalVolumeDescriptor()
- reserve_logical_volume.new()
- reserve_logical_volume.add_partition_map(1)
- self.udf_reserve_descs.logical_volumes.append(reserve_logical_volume)
-
- reserve_unallocated_space = udfmod.UDFUnallocatedSpaceDescriptor()
- reserve_unallocated_space.new()
- self.udf_reserve_descs.unallocated_space.append(reserve_unallocated_space)
-
- self.udf_reserve_descs.terminator.new()
-
- num_bytes_to_add += 16 * self.logical_block_size
-
- # Create the Logical Volume Integrity Sequence.
- self.udf_logical_volume_integrity = udfmod.UDFLogicalVolumeIntegrityDescriptor()
- self.udf_logical_volume_integrity.new()
-
- self.udf_logical_volume_integrity_terminator = udfmod.UDFTerminatingDescriptor()
- self.udf_logical_volume_integrity_terminator.new()
-
- num_bytes_to_add += 192 * self.logical_block_size
-
- # Create the Anchor.
- anchor1 = udfmod.UDFAnchorVolumeStructure()
- anchor1.new()
- self.udf_anchors.append(anchor1)
-
- num_bytes_to_add += self.logical_block_size
-
- # Create the File Set
- self.udf_file_set.new()
-
- self.udf_file_set_terminator = udfmod.UDFTerminatingDescriptor()
- self.udf_file_set_terminator.new()
-
- num_bytes_to_add += 2 * self.logical_block_size
-
- # Create the root directory, and the 'parent' entry inside.
- self.udf_root = udfmod.UDFFileEntry()
- self.udf_root.new(0, 'dir', None, self.logical_block_size)
- num_bytes_to_add += self.logical_block_size
-
- parent = udfmod.UDFFileIdentifierDescriptor()
- parent.new(True, True, b'', None)
- num_new_extents = self.udf_root.add_file_ident_desc(parent,
- self.logical_block_size)
- num_bytes_to_add += num_new_extents * self.logical_block_size
-
- num_partition_bytes_to_add = 0
- # Create the PTR, and add the 4 extents that comprise of the LE PTR and
- # BE PTR to the number of bytes to add.
- ptr = path_table_record.PathTableRecord()
- ptr.new_root()
- self.pvd.root_directory_record().set_ptr(ptr)
- num_partition_bytes_to_add += 4 * self.logical_block_size
-
- # Also add one extent to the size for the root directory record.
- num_partition_bytes_to_add += self.logical_block_size
-
- self._create_dot(self.pvd, self.pvd.root_directory_record(),
- self.rock_ridge, self.xa, 0o040555)
- self._create_dotdot(self.pvd, self.pvd.root_directory_record(),
- self.rock_ridge, False, self.xa, 0o040555)
-
- if self.joliet_vd is not None:
- # Create the PTR, and add the 4 extents that comprise of the LE PTR
- # and BE PTR to the number of bytes to add.
- ptr = path_table_record.PathTableRecord()
- ptr.new_root()
- self.joliet_vd.root_directory_record().set_ptr(ptr)
- num_partition_bytes_to_add += 4 * self.logical_block_size
-
- # Also add one extent to the size for the root directory record.
- num_partition_bytes_to_add += self.logical_block_size
-
- self._create_dot(self.joliet_vd,
- self.joliet_vd.root_directory_record(), '',
- False, -1)
- self._create_dotdot(self.joliet_vd,
- self.joliet_vd.root_directory_record(), '',
- False, False, -1)
-
- if self.rock_ridge:
- num_partition_bytes_to_add += self.logical_block_size
-
- if udf:
- anchor2 = udfmod.UDFAnchorVolumeStructure()
- anchor2.new()
- self.udf_anchors.append(anchor2)
-
- num_partition_bytes_to_add += self.logical_block_size
-
- self._finish_add(num_bytes_to_add, num_partition_bytes_to_add)
-
- self._initialized = True
-
- def open(self, filename, mode='rb'):
- # type: (str, str) -> None
- """
- Open up an existing ISO for inspection and modification.
-
- Parameters:
- filename - The filename containing the ISO to open up.
- mode - The mode to use when opening the ISO file; the default is 'rb'.
- Returns:
- Nothing.
- """
- if self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object already has an ISO; either close it or create a new object')
-
- fp = open(filename, mode) # pylint: disable=consider-using-with,unspecified-encoding
- self._managing_fp = True
- try:
- self._open_fp(fp)
- except Exception:
- fp.close()
- raise
-
- def open_fp(self, fp):
- # type: (BinaryIO) -> None
- """
- Open up an existing ISO for inspection and modification. Note that the
- file object passed in here must stay open for the lifetime of this
- object, as the PyCdlib class uses it internally to do writing and reading
- operations. To have PyCdlib manage this automatically, use 'open'
- instead.
-
- Parameters:
- fp - The file object containing the ISO to open up.
- Returns:
- Nothing.
- """
- if self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object already has an ISO; either close it or create a new object')
-
- self._open_fp(fp)
-
- def get_file_from_iso(self, local_path, **kwargs):
- # type: (str, Union[str, int]) -> None
- """
- Fetch a single file from the ISO and write it out to a local file.
-
- Parameters:
- local_path - The local file to write to.
- blocksize - The number of bytes in each transfer.
- iso_path - The absolute ISO9660 path to lookup on the ISO (exclusive
- with rr_path, joliet_path, and udf_path).
- rr_path - The absolute Rock Ridge path to lookup on the ISO (exclusive
- with iso_path, joliet_path, and udf_path).
- joliet_path - The absolute Joliet path to lookup on the ISO (exclusive
- with iso_path, rr_path, and udf_path).
- udf_path - The absolute UDF path to lookup on the ISO (exclusive with
- iso_path, rr_path, and joliet_path).
- encoding - The encoding to use for parsing the filenames.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- blocksize = 8192
- joliet_path = None
- iso_path = None
- rr_path = None
- udf_path = None
- encoding = ''
- num_paths = 0
- for key, value in kwargs.items():
- if key == 'blocksize':
- if not isinstance(value, int):
- raise pycdlibexception.PyCdlibInvalidInput('blocksize must be an integer')
- blocksize = value
- elif key == 'iso_path':
- if isinstance(value, str):
- iso_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('iso_path must be a string')
- elif key == 'rr_path':
- if isinstance(value, str):
- rr_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('iso_path must be a string')
- elif key == 'joliet_path':
- if isinstance(value, str):
- joliet_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('iso_path must be a string')
- elif key == 'udf_path':
- if isinstance(value, str):
- udf_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('iso_path must be a string')
- elif key == 'encoding':
- if not isinstance(value, str):
- raise pycdlibexception.PyCdlibInvalidInput('encoding must be a string')
- encoding = value
- else:
- raise pycdlibexception.PyCdlibInvalidInput('Unknown keyword %s' % (key))
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Exactly one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path' must be passed")
-
- with open(local_path, 'wb') as fp:
- if udf_path is not None:
- self._udf_get_file_from_iso_fp(fp, blocksize, udf_path)
- else:
- self._get_file_from_iso_fp(fp, blocksize, iso_path, rr_path,
- joliet_path, encoding)
-
- def get_file_from_iso_fp(self, outfp, **kwargs):
- # type: (BinaryIO, Union[str, int]) -> None
- """
- Fetch a single file from the ISO and write it out to the file object.
-
- Parameters:
- outfp - The file object to write data to.
- blocksize - The number of bytes in each transfer.
- iso_path - The absolute ISO9660 path to lookup on the ISO (exclusive
- with rr_path, joliet_path, and udf_path).
- rr_path - The absolute Rock Ridge path to lookup on the ISO (exclusive
- with iso_path, joliet_path, and udf_path).
- joliet_path - The absolute Joliet path to lookup on the ISO (exclusive
- with iso_path, rr_path, and udf_path).
- udf_path - The absolute UDF path to lookup on the ISO (exclusive with
- iso_path, rr_path, and joliet_path).
- encoding - The encoding to use for parsing the filenames.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- blocksize = 8192
- joliet_path = None
- iso_path = None
- rr_path = None
- udf_path = None
- encoding = None
- num_paths = 0
- for key, value in kwargs.items():
- if key == 'blocksize':
- if not isinstance(value, int):
- raise pycdlibexception.PyCdlibInvalidInput('blocksize must be an integer')
- blocksize = value
- elif key == 'iso_path':
- if isinstance(value, str):
- iso_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('iso_path must be a string')
- elif key == 'rr_path':
- if isinstance(value, str):
- rr_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('rr_path must be a string')
- elif key == 'joliet_path':
- if isinstance(value, str):
- joliet_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('joliet_path must be a string')
- elif key == 'udf_path':
- if isinstance(value, str):
- udf_path = utils.normpath(value)
- num_paths += 1
- elif value is not None:
- raise pycdlibexception.PyCdlibInvalidInput('udf_path must be a string')
- elif key == 'encoding':
- if not isinstance(value, str):
- raise pycdlibexception.PyCdlibInvalidInput('encoding must be a string')
- encoding = value
- else:
- raise pycdlibexception.PyCdlibInvalidInput('Unknown keyword %s' % (key))
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Exactly one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path' must be passed")
-
- if udf_path is not None:
- self._udf_get_file_from_iso_fp(outfp, blocksize, udf_path)
- else:
- self._get_file_from_iso_fp(outfp, blocksize, iso_path, rr_path,
- joliet_path, encoding)
-
- def get_and_write(self, iso_path, local_path, blocksize=8192):
- # type: (str, str, int) -> None
- """
- (deprecated) Fetch a single file from the ISO and write it out to the
- specified file. Note that this will overwrite the contents of the local
- file if it already exists. Also note that 'iso_path' must be an
- absolute path to the file. Finally, the 'iso_path' can be an ISO9660
- path, a Rock Ridge path, or a Joliet path. In the case of ambiguity,
- the Joliet path is tried first, followed by the ISO9660 path, followed
- by the Rock Ridge path. It is recommended to use the get_file_from_iso
- API instead to resolve this ambiguity.
-
- Parameters:
- iso_path - The absolute path to the file to get data from.
- local_path - The local filename to write the contents to.
- blocksize - The blocksize to use when copying data; the default is 8192.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- with open(local_path, 'wb') as fp:
- self._get_and_write_fp(utils.normpath(iso_path), fp, blocksize)
-
- def get_and_write_fp(self, iso_path, outfp, blocksize=8192):
- # type: (str, BinaryIO, int) -> None
- """
- (deprecated) Fetch a single file from the ISO and write it out to the
- file object. Note that 'iso_path' must be an absolute path to the file.
- Also note that the 'iso_path' can be an ISO9660 path, a Rock Ridge path,
- or a Joliet path. In the case of ambiguity, the Joliet path is tried
- first, followed by the ISO9660 path, followed by the Rock Ridge path.
- It is recommend to use the get_file_from_iso_fp API instead to resolve
- this ambiguity.
-
- Parameters:
- iso_path - The absolute path to the file to get data from.
- outfp - The file object to write data to.
- blocksize - The blocksize to use when copying data; the default is 8192.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- self._get_and_write_fp(utils.normpath(iso_path), outfp, blocksize)
-
- def write(self, filename, blocksize=32768, progress_cb=None,
- progress_opaque=None):
- # type: (str, int, Optional[Callable[[int, int, Any], None]], Optional[Any]) -> None
- """
- Write a properly formatted ISO out to the filename passed in. This
- also goes by the name of 'mastering'.
-
- Parameters:
- filename - The filename to write the data to.
- blocksize - The blocksize to use when copying data; the default is 32768.
- progress_cb - If not None, a function to call as the write call does its
- work. The callback function must have a signature of:
- def func(done, total, opaque).
- progress_opaque - User data to be passed to the progress callback; the
- default is None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- with open(filename, 'wb') as fp:
- self._write_fp(fp, blocksize, progress_cb, progress_opaque)
-
- def write_fp(self, outfp, blocksize=32768, progress_cb=None,
- progress_opaque=None):
- # type: (BinaryIO, int, Optional[Callable[[int, int, Any], None]], Optional[Any]) -> None
- """
- Write a properly formatted ISO out to the file object passed in. This
- also goes by the name of 'mastering'.
-
- Parameters:
- outfp - The file object to write the data to.
- blocksize - The blocksize to use when copying data; the default is 32768.
- progress_cb - If not None, a function to call as the write call does its
- work. The callback function must have a signature of:
- def func(done, total, opaque).
- progress_opaque - User data to be passed to the progress callback; the
- default is None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- self._write_fp(outfp, blocksize, progress_cb, progress_opaque)
-
- def add_fp(self, fp, length, iso_path=None, rr_name=None, joliet_path=None,
- file_mode=None, udf_path=None):
- # type: (BinaryIO, int, Optional[str], Optional[str], Optional[str], Optional[int], Optional[str]) -> None
- """
- Add a file to the ISO. If the ISO is a Rock Ridge one, then a Rock
- Ridge name must also be provided. If the ISO is a Joliet one, then a
- Joliet path may also be provided; while it is optional to do so, it is
- highly recommended. Note that the caller must ensure that 'fp' remains
- open for the lifetime of the PyCdlib object, as the PyCdlib class uses
- the file descriptor internally when writing (mastering) the ISO. To
- have PyCdlib manage this automatically, use 'add_file' instead.
-
- Parameters:
- fp - The file object to use for the contents of the new file.
- length - The length of the data for the new file.
- iso_path - The ISO9660 absolute path to the file destination on the ISO.
- rr_name - The Rock Ridge name of the file destination on the ISO.
- joliet_path - The Joliet absolute path to the file destination on the ISO.
- file_mode - The POSIX file_mode to apply to this file. This only
- applies if this is a Rock Ridge ISO. If this is None (the
- default), the permissions from the original file are used.
- udf_path - The UDF name of the file destination on the ISO.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if not utils.file_object_supports_binary(fp):
- raise pycdlibexception.PyCdlibInvalidInput('The fp argument must be in binary mode')
-
- num_bytes_to_add = self._add_fp(fp, length, False, iso_path, rr_name,
- joliet_path, udf_path, file_mode, False)
-
- self._finish_add(0, num_bytes_to_add)
-
- def add_file(self, filename, iso_path=None, rr_name=None, joliet_path=None,
- file_mode=None, udf_path=None):
- # type: (str, Optional[str], Optional[str], Optional[str], Optional[int], Optional[str]) -> None
- """
- Add a file to the ISO. If the ISO is a Rock Ridge one, then a Rock
- Ridge name must also be provided. If the ISO is a Joliet one, then a
- Joliet path may also be provided; while it is optional to do so, it is
- highly recommended.
-
- Parameters:
- filename - The filename to use for the data contents for the new file.
- iso_path - The ISO9660 absolute path to the file destination on the ISO.
- rr_name - The Rock Ridge name of the file destination on the ISO.
- joliet_path - The Joliet absolute path to the file destination on the ISO.
- file_mode - The POSIX file_mode to apply to this file. This only
- applies if this is a Rock Ridge ISO. If this is None (the
- default), the permissions from the original file are used.
- udf_path - The UDF name of the file destination on the ISO.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_bytes_to_add = self._add_fp(filename, os.stat(filename).st_size,
- True, iso_path, rr_name, joliet_path,
- udf_path, file_mode, False)
-
- self._finish_add(0, num_bytes_to_add)
-
- def modify_file_in_place(self, fp, length, iso_path, rr_name=None, # pylint: disable=unused-argument
- joliet_path=None, udf_path=None): # pylint: disable=unused-argument
- # type: (BinaryIO, int, str, Optional[str], Optional[str], Optional[str]) -> None
- """
- An API to modify a file in place on the ISO. This can be extremely fast
- (much faster than calling the write method), but has many restrictions.
-
- 1. The original ISO file pointer must have been opened for reading
- and writing.
- 2. Only an existing *file* can be modified; directories cannot be
- changed.
- 3. Only an existing file can be *modified*; no new files can be added
- or removed.
- 4. The new file contents must use the same number of extents (typically
- 2048 bytes) as the old file contents. If using this API to shrink
- a file, this is usually easy since the new contents can be padded
- out with zeros or newlines to meet the requirement. If using this
- API to grow a file, the new contents can only grow up to the next
- extent boundary.
-
- Unlike all other APIs in PyCdlib, this API actually modifies the
- originally opened on-disk file, so use it with caution.
-
- Parameters:
- fp - The file object to use for the contents of the new file.
- length - The length of the new data for the file.
- iso_path - The ISO9660 absolute path to the file destination on the ISO.
- rr_name - The Rock Ridge name of the file destination on the ISO.
- joliet_path - The Joliet absolute path to the file destination on the ISO.
- udf_path - The UDF absolute path to the file destination on the ISO.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if hasattr(self._cdfp, 'mode') and not self._cdfp.mode.startswith(('r+', 'w', 'a', 'rb+')):
- raise pycdlibexception.PyCdlibInvalidInput('To modify a file in place, the original ISO must have been opened in a write mode (r+, w, or a)')
-
- child = self._find_iso_record(utils.normpath(iso_path))
-
- old_num_extents = utils.ceiling_div(child.get_data_length(),
- self.logical_block_size)
- new_num_extents = utils.ceiling_div(length, self.logical_block_size)
-
- if old_num_extents != new_num_extents:
- raise pycdlibexception.PyCdlibInvalidInput('When modifying a file in-place, the number of extents for a file cannot change!')
-
- if not child.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot modify a directory with modify_file_in_place')
-
- if child.inode is None:
- raise pycdlibexception.PyCdlibInternalError('Child file found without inode')
-
- child.inode.update_fp(fp, length)
-
- # Remove the old size from the PVD size.
- for pvd in self.pvds:
- pvd.remove_from_space_size(child.get_data_length())
- # And add the new size to the PVD size.
- for pvd in self.pvds:
- pvd.add_to_space_size(length)
-
- if self.enhanced_vd is not None:
- self.enhanced_vd.copy_sizes(self.pvd)
-
- # If we made it here, we have successfully updated all of the in-memory
- # metadata. Now we can go and modify the on-disk file.
-
- self._seek_to_extent(self.pvd.extent_location())
-
- # First write out the PVD.
- rec = self.pvd.record()
- self._cdfp.write(rec)
-
- # Write out the joliet VD.
- if self.joliet_vd is not None:
- self._seek_to_extent(self.joliet_vd.extent_location())
- rec = self.joliet_vd.record()
- self._cdfp.write(rec)
-
- # Write out the enhanced VD.
- if self.enhanced_vd is not None:
- self._seek_to_extent(self.enhanced_vd.extent_location())
- rec = self.enhanced_vd.record()
- self._cdfp.write(rec)
-
- # We don't have to write anything out for UDF since it only tracks
- # extents, and we know we aren't changing the number of extents.
-
- # Write out the actual file contents.
- self._seek_to_extent(child.extent_location())
- with inode.InodeOpenData(child.inode, self.logical_block_size) as (data_fp, data_len):
- utils.copy_data(data_len, self.logical_block_size, data_fp, self._cdfp)
- utils.zero_pad(self._cdfp, data_len, self.logical_block_size)
-
- # Finally write out the directory record entry.
- # This is a little tricky because of what things mean. First of all,
- # child.extents_to_here represents the total number of extents up to
- # this child in the parent. Thus, to get the absolute extent offset,
- # we start with the parent's extent location, add on the number of
- # extents to here, and remove 1 (since our offset will be zero-based).
- # Second, child.offset_to_here is the *last* byte that the child uses,
- # so to get the start of it we subtract off the length of the child.
- # Then we can multiply the extent location by the logical block size,
- # add on the offset, and get to the absolute location in the file.
- first_joliet = True
- for record, is_pvd_unused in child.inode.linked_records:
- if isinstance(record, dr.DirectoryRecord):
- if self.joliet_vd is not None and id(record.vd) == id(self.joliet_vd) and first_joliet:
- first_joliet = False
- self.joliet_vd.remove_from_space_size(record.get_data_length())
- self.joliet_vd.add_to_space_size(length)
- if record.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Modifying file with empty parent')
- abs_extent_loc = record.parent.extent_location() + record.extents_to_here - 1
- offset = record.offset_to_here - record.dr_len
- abs_offset = abs_extent_loc * self.logical_block_size + offset
- elif isinstance(record, udfmod.UDFFileEntry):
- abs_offset = record.extent_location() * self.logical_block_size
- else:
- # This should never happen
- raise pycdlibexception.PyCdlibInternalError('Invalid record type')
-
- record.set_data_length(length)
- self._cdfp.seek(abs_offset)
- self._cdfp.write(record.record())
-
- def add_hard_link(self, **kwargs):
- # type: (str) -> None
- """
- Add a hard link to the ISO. Hard links are alternate names for the
- same file contents that don't take up any additional space on the the
- ISO. This API can be used to create hard links between two files on
- the ISO9660 filesystem, between two files on the Joliet filesystem, or
- between a file on the ISO9660 filesystem and the Joliet filesystem.
- In all cases, exactly one old path must be specified, and exactly one
- new path must be specified.
- Note that this is an advanced API, so using it in combination with the
- higher-level APIs (like rm_file()) may result in unexpected behavior.
- Once this API has been used, this API and rm_hard_link() should be
- preferred over add_file() and rm_file(), respectively.
-
- Parameters:
- iso_old_path - The old path on the ISO9660 filesystem to link from.
- iso_new_path - The new path on the ISO9660 filesystem to link to.
- joliet_old_path - The old path on the Joliet filesystem to link from.
- joliet_new_path - The new path on the Joliet filesystem to link to.
- rr_name - The Rock Ridge name to use for the new file if this is a Rock
- Ridge ISO and the new path is on the ISO9660 filesystem.
- boot_catalog_old - Use the El Torito boot catalog as the old path.
- udf_old_path - The old path on the UDF filesystem to link from.
- udf_new_path - The new path on the UDF filesystem to link to.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_old = 0
- iso_old_path = None
- joliet_old_path = None
- boot_catalog_old = False
- udf_old_path = None
- keys_to_remove = []
- for key, value in kwargs.items():
- if key == 'iso_old_path':
- if value is not None:
- num_old += 1
- iso_old_path = utils.normpath(value)
- keys_to_remove.append(key)
- elif key == 'joliet_old_path':
- if value is not None:
- num_old += 1
- joliet_old_path = self._normalize_joliet_path(value)
- keys_to_remove.append(key)
- elif key == 'boot_catalog_old':
- if value:
- num_old += 1
- boot_catalog_old = True
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInvalidInput('Attempting to make link to non-existent El Torito boot catalog')
- keys_to_remove.append(key)
- elif key == 'udf_old_path':
- if value is not None:
- num_old += 1
- udf_old_path = utils.normpath(value)
- keys_to_remove.append(key)
-
- if num_old != 1:
- raise pycdlibexception.PyCdlibInvalidInput('Exactly one old path must be specified')
-
- # Once we've iterated over the keys we know about, remove them from
- # the map so that _add_hard_link_to_inode() can parse the rest.
- for key in keys_to_remove:
- del kwargs[key]
-
- # It would be nice to allow the addition of a link to the El Torito
- # Initial/Default Entry. Unfortunately, the information we need for
- # a 'hidden' Initial entry just doesn't exist on the ISO. In
- # particular, we don't know the real size that the file should be, we
- # only know the number of emulated sectors (512 bytes) that it will be
- # loaded into. Since the true length and the number of sectors are not
- # the same thing, we can't actually add a hard link.
-
- old_rec = dr.DirectoryRecord() # type: Union[dr.DirectoryRecord, udfmod.UDFFileEntry]
- fmode = 0
- if iso_old_path is not None:
- # A link from a file on the ISO9660 filesystem...
- old_rec = self._find_iso_record(iso_old_path)
- if old_rec.rock_ridge is not None:
- fmode = old_rec.rock_ridge.get_file_mode()
- elif joliet_old_path is not None:
- # A link from a file on the Joliet filesystem...
- old_rec = self._find_joliet_record(joliet_old_path)
- elif boot_catalog_old:
- # A link from the El Torito boot catalog...
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInvalidInput('Attempting to make link to non-existent El Torito boot catalog')
- old_rec = self.eltorito_boot_catalog.dirrecords[0]
- elif udf_old_path is not None:
- # A link from a file on the UDF filesystem...
- (old_ident_unused, old_rec) = self._find_udf_record(udf_old_path)
- if old_rec is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot make hard link to a UDF file with an empty UDF File Entry')
-
- # Above we checked to make sure we got at least one old path, so we
- # don't need to worry about the else situation here.
-
- num_bytes_to_add = self._add_hard_link_to_inode(old_rec.inode,
- old_rec.get_data_length(),
- fmode, boot_catalog_old,
- **kwargs)
-
- self._finish_add(0, num_bytes_to_add)
-
- def rm_hard_link(self, iso_path=None, joliet_path=None, udf_path=None):
- # type: (Optional[str], Optional[str], Optional[str]) -> None
- """
- Remove a hard link from the ISO. If the number of links to a piece of
- data drops to zero, then the contents will be removed from the ISO.
- This can be thought of as a lower-level interface to rm_file(). Either
- an ISO9660 path or a Joliet path must be passed to this API, but not
- both. Thus, this interface can be used to hide files from either the
- ISO9660 filesystem, the Joliet filesystem, or both (if there is another
- reference to the data on the ISO, such as in El Torito). Note that this
- is an advanced API, so using it in combination with the higher-level APIs
- (like rm_file()) may result in unexpected behavior. Once this API has
- been used, this API and add_hard_link() should be preferred over
- rm_file() and add_file().
-
- Parameters:
- iso_path - The ISO link path to remove.
- joliet_path - The Joliet link path to remove.
- udf_path - The UDF link path to remove.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if len([x for x in (iso_path, joliet_path, udf_path) if x]) != 1:
- raise pycdlibexception.PyCdlibInvalidInput('Must provide exactly one of iso_path, joliet_path, or udf_path')
-
- num_bytes_to_remove = 0
- rec = None # type: Optional[Union[dr.DirectoryRecord, udfmod.UDFFileEntry]]
-
- if iso_path is not None:
- rec = self._find_iso_record(utils.normpath(iso_path))
- num_bytes_to_remove += self._rm_dr_link(rec)
- elif joliet_path is not None:
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove Joliet link from non-Joliet ISO')
- joliet_path_bytes = self._normalize_joliet_path(joliet_path)
- rec = self._find_joliet_record(joliet_path_bytes)
- num_bytes_to_remove += self._rm_dr_link(rec)
- elif udf_path is not None:
- # UDF hard link removal
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
-
- (ident, rec) = self._find_udf_record(utils.normpath(udf_path))
- if rec is None:
- # If the rec is None, that means that this pointed to an 'empty'
- # UDF File Entry. Just remove the UDF File Identifier, which is
- # as much as we can do.
- if ident is not None and ident.parent is not None:
- num_bytes_to_remove += self._rm_udf_file_ident(ident.parent, ident.fi)
- # We also have to remove the "zero" UDF File Entry, since nothing
- # else will.
- num_bytes_to_remove += self.logical_block_size
- else:
- num_bytes_to_remove += self._rm_udf_link(rec)
- else:
- raise pycdlibexception.PyCdlibInvalidInput("One of 'iso_path', 'joliet_path', or 'udf_path' must be specified")
-
- self._finish_remove(num_bytes_to_remove, True)
-
- def add_directory(self, iso_path=None, rr_name=None, joliet_path=None,
- file_mode=None, udf_path=None):
- # type: (Optional[str], Optional[str], Optional[str], Optional[int], Optional[str]) -> None
- """
- Add a directory to the ISO. At least one of an iso_path, joliet_path,
- or udf_path must be provided. Providing joliet_path on a non-Joliet
- ISO, or udf_path on a non-UDF ISO, is an error. If the ISO contains
- Rock Ridge, then a Rock Ridge name must be provided.
-
- Parameters:
- iso_path - The ISO9660 absolute path to use for the directory.
- rr_name - The Rock Ridge name to use for the directory.
- joliet_path - The Joliet absolute path to use for the directory.
- file_mode - The POSIX file mode to use for the directory. This only
- applies for Rock Ridge ISOs.
- udf_path - The UDF absolute path to use for the directory.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if iso_path is None and joliet_path is None and udf_path is None:
- raise pycdlibexception.PyCdlibInvalidInput('Either iso_path or joliet_path must be passed')
-
- if file_mode is not None and not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('A file mode can only be specified for Rock Ridge ISOs')
-
- # For backwards-compatibility reasons, if the mode was not specified we
- # just assume 555. We should probably eventually make file_mode
- # required for Rock Ridge and remove this assumption.
- if file_mode is None:
- file_mode = 0o040555
-
- num_bytes_to_add = 0
- if iso_path is not None:
- iso_path_bytes = utils.normpath(iso_path)
-
- new_rr_name = self._check_rr_name(rr_name)
-
- depth = len(utils.split_path(iso_path_bytes))
-
- if not self.rock_ridge and self.enhanced_vd is None:
- _check_path_depth(iso_path_bytes)
- (name, parent) = self._iso_name_and_parent_from_path(iso_path_bytes)
-
- _check_iso9660_directory(name, self.interchange_level)
-
- relocated = False
- fake_dir_rec = None
- orig_parent = None
- iso9660_name = name
- if self.rock_ridge and (depth % 8) == 0 and self.enhanced_vd is None:
- # If the depth was a multiple of 8, then we are going to have to
- # make a relocated entry for this record.
-
- num_bytes_to_add += self._find_or_create_rr_moved()
-
- # With a depth of 8, we have to add the directory both to the
- # original parent with a CL link, and to the new parent with an
- # RE link. Here we make the 'fake' record, as a child of the
- # original place; the real one will be done below.
- fake_dir_rec = dr.DirectoryRecord()
- fake_dir_rec.new_dir(self.pvd, name, parent,
- self.pvd.sequence_number(),
- self.rock_ridge, new_rr_name,
- self.logical_block_size, True, False,
- self.xa, file_mode, time.time())
- num_bytes_to_add += self._add_child_to_dr(fake_dir_rec)
-
- # The fake dir record doesn't get an entry in the path table
- # record.
-
- relocated = True
- orig_parent = parent
- parent = self._rr_moved_record
-
- # Since we are moving the entry underneath the RR_MOVED
- # directory, there is now the chance of a name collision (this
- # can't happen without relocation since _add_child_to_dr() below
- # won't allow duplicate names). Check for that here and
- # generate a new name.
- index = 0
- while True:
- for child in self._rr_moved_record.children:
- if child.file_ident == iso9660_name:
- # Python 3.4 doesn't support substitution with a byte
- # array, so we do it as a string and encode to bytes.
- iso9660_name = name + ('%03d' % (index)).encode()
- index += 1
- break
- else:
- break
-
- rec = dr.DirectoryRecord()
- rec.new_dir(self.pvd, iso9660_name, parent,
- self.pvd.sequence_number(), self.rock_ridge, new_rr_name,
- self.logical_block_size, False, relocated,
- self.xa, file_mode, time.time())
- num_bytes_to_add += self._add_child_to_dr(rec)
- if rec.rock_ridge is not None:
- if relocated and fake_dir_rec is not None and fake_dir_rec.rock_ridge is not None:
- fake_dir_rec.rock_ridge.cl_to_moved_dr = rec
- rec.rock_ridge.moved_to_cl_dr = fake_dir_rec
- num_bytes_to_add += self._update_rr_ce_entry(rec)
-
- self._create_dot(self.pvd, rec, self.rock_ridge, self.xa, file_mode)
-
- parent_file_mode = -1
- if parent.rock_ridge is not None:
- parent_file_mode = parent.rock_ridge.get_file_mode()
- else:
- if parent.is_root:
- parent_file_mode = file_mode
-
- dotdot = self._create_dotdot(self.pvd, rec, self.rock_ridge,
- relocated, self.xa, parent_file_mode)
- if dotdot.rock_ridge is not None and relocated:
- dotdot.rock_ridge.parent_link = orig_parent
-
- # We always need to add an entry to the path table record.
- ptr = path_table_record.PathTableRecord()
- ptr.new_dir(iso9660_name)
-
- num_bytes_to_add += self._add_to_ptr_size(ptr) + self.logical_block_size
-
- rec.set_ptr(ptr)
-
- if joliet_path is not None:
- num_bytes_to_add += self._add_joliet_dir(self._normalize_joliet_path(joliet_path))
-
- if udf_path is not None:
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
-
- udf_path_bytes = utils.normpath(udf_path)
- (udf_name, udf_parent) = self._udf_name_and_parent_from_path(udf_path_bytes)
-
- file_ident = udfmod.UDFFileIdentifierDescriptor()
- file_ident.new(True, False, udf_name, udf_parent)
- num_new_extents = udf_parent.add_file_ident_desc(file_ident, self.logical_block_size)
- num_bytes_to_add += num_new_extents * self.logical_block_size
-
- file_entry = udfmod.UDFFileEntry()
- file_entry.new(0, 'dir', udf_parent, self.logical_block_size)
- file_ident.file_entry = file_entry
- file_entry.file_ident = file_ident
- num_bytes_to_add += self.logical_block_size
-
- udf_dotdot = udfmod.UDFFileIdentifierDescriptor()
- udf_dotdot.new(True, True, b'', udf_parent)
- num_new_extents = file_ident.file_entry.add_file_ident_desc(udf_dotdot, self.logical_block_size)
- num_bytes_to_add += num_new_extents * self.logical_block_size
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_impl_use.num_dirs += 1
-
- self._finish_add(0, num_bytes_to_add)
-
- def add_joliet_directory(self, joliet_path):
- # type: (str) -> None
- """
- (deprecated) Add a directory to the Joliet portion of the ISO. Since
- Joliet occupies a completely different context than ISO9660, this
- method can be invoked to create a completely different directory
- structure in the Joliet context, though that is generally not advised.
- It is recommended to use the 'joliet_path' argument of the
- 'add_directory' instead of this method.
-
- Parameters:
- joliet_path - The Joliet directory to create.
- Returns:
- Nothing.
- """
- self.add_directory(joliet_path=joliet_path)
-
- def rm_file(self, iso_path=None, rr_name=None, joliet_path=None, # pylint: disable=unused-argument
- udf_path=None):
- # type: (Optional[str], Optional[str], Optional[str], Optional[str]) -> None
- """
- Remove a file from the ISO. This removes the data and the listing of
- the file from all contexts, even when only one path is given (to only
- remove it from a single context, use rm_hard_link() instead). Due to
- some complexities of the ISO format, removal of zero-byte files from all
- contexts does not automatically happen, so this method may need to be
- called more than once for zero-byte files.
-
- Parameters:
- iso_path - The path to the file to remove.
- rr_name - The Rock Ridge name of the file to remove.
- joliet_path - The Joliet path to the file to remove.
- udf_path - The UDF path to the file to remove.
- Returns:
- Nothing.
- """
-
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_bytes_to_remove = 0
- if iso_path is not None:
- num_bytes_to_remove += self._rm_file_via_iso_path(iso_path)
- elif joliet_path is not None:
- num_bytes_to_remove += self._rm_file_via_joliet_path(joliet_path)
- elif udf_path is not None:
- num_bytes_to_remove += self._rm_file_via_udf_path(udf_path)
- else:
- raise pycdlibexception.PyCdlibInternalError("At least one of 'iso_path', 'joliet_path', or 'udf_path' must be specified")
-
- self._finish_remove(num_bytes_to_remove, True)
-
- def rm_directory(self, iso_path=None, rr_name=None, joliet_path=None, # pylint: disable=unused-argument
- udf_path=None):
- # type: (Optional[str], Optional[str], Optional[str], Optional[str]) -> None
- """
- Remove a directory from the ISO. The directory must be empty.
-
- Parameters:
- iso_path - The path to the directory to remove.
- rr_name - The Rock Ridge name of the directory to remove.
- joliet_path - The Joliet path to the directory to remove.
- udf_path - The UDF absolute path to the directory to remove.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if iso_path is None and joliet_path is None and udf_path is None:
- raise pycdlibexception.PyCdlibInvalidInput('Either iso_path or joliet_path must be passed')
-
- num_bytes_to_remove = 0
-
- if iso_path is not None:
- iso_path_bytes = utils.normpath(iso_path)
-
- if iso_path_bytes == b'/':
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove base directory')
-
- child = self._find_iso_record(iso_path_bytes)
-
- if not child.is_dir():
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove a file with rm_directory (try rm_file instead)')
-
- if len(child.children) > 2:
- raise pycdlibexception.PyCdlibInvalidInput('Directory must be empty to use rm_directory')
-
- num_bytes_to_remove += self._remove_child_from_dr(child,
- child.index_in_parent)
-
- if child.ptr is not None:
- num_bytes_to_remove += self._remove_from_ptr_size(child.ptr)
-
- # Remove space for the directory itself.
- num_bytes_to_remove += child.get_data_length()
-
- if child.rock_ridge is not None and child.rock_ridge.relocated_record():
- # OK, this child was relocated. If the parent of this relocated
- # record is empty (only . and ..), we can remove it.
- parent = child.parent
- if parent is None:
- raise pycdlibexception.PyCdlibInternalError('Relocated child has empty parent; this should not be')
- if len(parent.children) == 2:
- if parent.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to remove a directory that has no parent; this should not happen')
- for index, c in enumerate(parent.parent.children):
- if c.file_ident == parent.file_ident:
- parent_index = index
- break
- else:
- raise pycdlibexception.PyCdlibInvalidISO('Could not find parent in its own parent!')
-
- num_bytes_to_remove += self._remove_child_from_dr(parent,
- parent_index)
-
- num_bytes_to_remove += parent.get_data_length()
- if parent.ptr is not None:
- num_bytes_to_remove += self._remove_from_ptr_size(parent.ptr)
-
- cl = child.rock_ridge.moved_to_cl_dr
- if cl is None:
- raise pycdlibexception.PyCdlibInternalError('Invalid child link record')
- if cl.parent is None:
- raise pycdlibexception.PyCdlibInternalError('Invalid parent to child link record; this should not be')
- for index, c in enumerate(cl.parent.children):
- if cl.file_ident == c.file_ident:
- clindex = index
- break
- else:
- raise pycdlibexception.PyCdlibInvalidISO('CL record does not exist')
-
- if cl.children:
- raise pycdlibexception.PyCdlibInvalidISO('Parent link should have no children!')
- num_bytes_to_remove += self._remove_child_from_dr(cl, clindex)
-
- # We do not remove additional space from the PVD for the
- # child_link record because it is a 'fake' record that has no
- # size.
-
- if child.rock_ridge is not None and child.rock_ridge.dr_entries.ce_record is not None and child.rock_ridge.ce_block is not None:
- child.rock_ridge.ce_block.remove_entry(child.rock_ridge.dr_entries.ce_record.offset_cont_area,
- child.rock_ridge.dr_entries.ce_record.len_cont_area)
-
- if joliet_path is not None:
- num_bytes_to_remove += self._rm_joliet_dir(self._normalize_joliet_path(joliet_path))
-
- if udf_path is not None:
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
-
- udf_path_bytes = utils.normpath(udf_path)
-
- if udf_path_bytes == b'/':
- raise pycdlibexception.PyCdlibInvalidInput('Cannot remove base directory')
-
- (udf_name, udf_parent) = self._udf_name_and_parent_from_path(udf_path_bytes)
-
- num_extents_to_remove = udf_parent.remove_file_ident_desc_by_name(udf_name,
- self.logical_block_size)
- # Remove space (if necessary) in the parent File Identifier
- # Descriptor area.
- num_bytes_to_remove += num_extents_to_remove * self.logical_block_size
- # Remove space for the File Entry.
- num_bytes_to_remove += self.logical_block_size
- # Remove space for the list of File Identifier Descriptors.
- num_bytes_to_remove += self.logical_block_size
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_impl_use.num_dirs -= 1
-
- self._find_udf_record.cache_clear() # pylint: disable=no-member
-
- self._finish_remove(num_bytes_to_remove, True)
-
- def rm_joliet_directory(self, joliet_path):
- # type: (str) -> None
- """
- (deprecated) Remove a Joliet directory from the ISO. It is recommended
- to use the 'joliet_path' parameter to 'rm_directory' instead.
-
- Parameters:
- joliet_path - The Joliet path to the directory to remove.
- Returns:
- Nothing.
- """
- self.rm_directory(joliet_path=joliet_path)
-
- def add_eltorito(self, bootfile_path, bootcatfile=None,
- rr_bootcatname=None, joliet_bootcatfile=None,
- boot_load_size=None, platform_id=0, boot_info_table=False,
- efi=False, media_name='noemul', bootable=True,
- boot_load_seg=0, udf_bootcatfile=None):
- # type: (str, Optional[str], Optional[str], Optional[str], Optional[int], int, bool, bool, str, bool, int, Optional[str]) -> None
- """
- Add an El Torito Boot Record, and associated files, to the ISO. The
- file that will be used as the bootfile must be passed into this function
- and must already be present on the ISO.
-
- Parameters:
- bootfile_path - The file to use as the boot file; it must already
- exist on this ISO.
- bootcatfile - The fake file to use as the boot catalog entry; set to
- BOOT.CAT;1 by default.
- rr_bootcatname - The Rock Ridge name for the fake file to use as the
- boot catalog entry; set to 'boot.cat' by default.
- joliet_bootcatfile - The Joliet name for the fake file to use as the
- boot catalog entry; set to 'boot.cat' by default.
- boot_load_size - The number of sectors to use for the boot entry; if
- set to None (the default), the number of sectors will
- be calculated.
- platform_id - The platform ID to set for the El Torito entry; 0 is for
- x86, 1 is for Power PC, 2 is for Mac, and 0xef is for
- UEFI. 0 is the default.
- boot_info_table - Whether to add a boot info table to the ISO. The
- default is False.
- efi - Whether this is an EFI entry for El Torito. The default is False.
- media_name - The name of the media type, one of 'noemul', 'floppy', or 'hdemul'.
- bootable - Whether the boot media is bootable. The default is True.
- boot_load_seg - The load segment address of the boot image.
- udf_bootcatfile - The name of the boot.cat file on the UDF filesystem.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- # In order to add an El Torito boot, we need to do the following:
- # 1. Find the boot file record (which must already exist).
- # 2. Construct a BootRecord.
- # 3. Construct a BootCatalog, and add it to the filesystem.
- # 4. Add the boot record to the ISO.
-
- if not bootcatfile:
- bootcatfile = '/BOOT.CAT;1'
-
- bootfile_path_bytes = utils.normpath(bootfile_path)
-
- if self.joliet_vd is not None:
- if not joliet_bootcatfile:
- joliet_bootcatfile = '/boot.cat'
- else:
- if joliet_bootcatfile:
- raise pycdlibexception.PyCdlibInvalidInput('A joliet path must not be passed when adding El Torito to a non-Joliet ISO')
-
- if self.udf_root is not None:
- if not udf_bootcatfile:
- udf_bootcatfile = '/boot.cat'
- else:
- if udf_bootcatfile:
- raise pycdlibexception.PyCdlibInvalidInput('A UDF path must not be passed when adding El Torito to a non-UDF ISO')
-
- # Step 1.
- boot_dirrecord = self._find_iso_record(bootfile_path_bytes)
-
- if boot_load_size is None:
- sector_count = utils.ceiling_div(boot_dirrecord.get_data_length(),
- self.logical_block_size) * self.logical_block_size // 512
- else:
- sector_count = boot_load_size
-
- if boot_dirrecord.inode is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to add an empty boot dirrecord inode to the El Torito boot catalog')
-
- if boot_info_table:
- orig_len = boot_dirrecord.get_data_length()
- bi_table = eltorito.EltoritoBootInfoTable()
- with inode.InodeOpenData(boot_dirrecord.inode, self.logical_block_size) as (data_fp, data_len):
- bi_table.new(self.pvd, boot_dirrecord.inode, orig_len,
- self._calculate_eltorito_boot_info_table_csum(data_fp, data_len))
-
- boot_dirrecord.inode.add_boot_info_table(bi_table)
-
- system_type = 0
- if media_name == 'hdemul':
- with inode.InodeOpenData(boot_dirrecord.inode, self.logical_block_size) as (data_fp, data_len):
- disk_mbr = data_fp.read(512)
- if len(disk_mbr) != 512:
- raise pycdlibexception.PyCdlibInvalidInput('Could not read entire HD MBR, must be at least 512 bytes')
- system_type = eltorito.hdmbrcheck(disk_mbr, sector_count,
- bootable)
-
- num_bytes_to_add = 0
- if self.eltorito_boot_catalog is not None:
- # An El Torito Boot Catalog already exists; add a new section.
- self.eltorito_boot_catalog.add_section(boot_dirrecord.inode,
- sector_count, boot_load_seg,
- media_name, system_type, efi,
- bootable)
- else:
- # Step 2.
- br = headervd.BootRecord()
- br.new(b'EL TORITO SPECIFICATION')
- self.brs.append(br)
- # On a UDF ISO, adding a new Boot Record doesn't actually increase
- # the size, since there are a bunch of gaps at the beginning.
- if not self._has_udf:
- num_bytes_to_add += self.logical_block_size
-
- # Step 3.
- self.eltorito_boot_catalog = eltorito.EltoritoBootCatalog(br)
- self.eltorito_boot_catalog.new(br, boot_dirrecord.inode,
- sector_count, boot_load_seg,
- media_name, system_type, platform_id,
- bootable)
-
- # Step 4.
- rrname = ''
- if self.rock_ridge:
- if rr_bootcatname is None:
- rrname = 'boot.cat'
- else:
- rrname = rr_bootcatname
-
- num_bytes_to_add += self._add_fp(None, self.logical_block_size,
- False, bootcatfile, rrname,
- joliet_bootcatfile,
- udf_bootcatfile, None, True)
-
- self._finish_add(0, num_bytes_to_add)
-
- def rm_eltorito(self):
- # type: () -> None
- """
- Remove the El Torito boot record (and Boot Catalog) from the ISO.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInvalidInput('This ISO does not have an El Torito Boot Record')
-
- for brindex, br in enumerate(self.brs):
- if br.boot_system_identifier == b'EL TORITO SPECIFICATION'.ljust(32, b'\x00'):
- eltorito_index = brindex
- break
- else:
- # There was a boot catalog, but no corresponding boot record. This
- # should never happen.
- raise pycdlibexception.PyCdlibInternalError('El Torito boot catalog found with no corresponding boot record')
-
- del self.brs[eltorito_index]
-
- num_bytes_to_remove = 0
-
- # On a UDF ISO, removing the Boot Record doesn't actually decrease
- # the size, since there are a bunch of gaps at the beginning.
- if not self._has_udf:
- num_bytes_to_remove += self.logical_block_size
-
- # Remove all of the DirectoryRecord/UDFFileEntries associated with
- # the Boot Catalog.
- for rec in self.eltorito_boot_catalog.dirrecords:
- if isinstance(rec, dr.DirectoryRecord):
- num_bytes_to_remove += self._rm_dr_link(rec)
- elif isinstance(rec, udfmod.UDFFileEntry):
- num_bytes_to_remove += self._rm_udf_link(rec)
- else:
- # This should never happen.
- raise pycdlibexception.PyCdlibInternalError('Saw an El Torito record that was neither ISO nor UDF')
-
- # Remove the linkage from the El Torito Entries to the inodes.
- entries_to_remove = [self.eltorito_boot_catalog.initial_entry]
- for sec in self.eltorito_boot_catalog.sections:
- for entry in sec.section_entries:
- entries_to_remove.append(entry)
-
- for entry in entries_to_remove:
- if entry.inode is not None:
- new_list = []
- for linkrec, is_pvd in entry.inode.linked_records:
- if id(linkrec) != id(entry):
- new_list.append((linkrec, is_pvd))
- entry.inode.linked_records = new_list
-
- num_bytes_to_remove += len(self.eltorito_boot_catalog.record())
-
- self.eltorito_boot_catalog = None
-
- self._finish_remove(num_bytes_to_remove, True)
-
- def add_symlink(self, symlink_path=None, rr_symlink_name=None, rr_path=None,
- joliet_path=None, udf_symlink_path=None, udf_target=None):
- # type: (Optional[str], Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]) -> None
- """
- Add a symlink from rr_symlink_name to the rr_path. The ISO must have
- either Rock Ridge or UDF support (or both).
-
- Parameters:
- symlink_path - The ISO9660 path of the symlink itself on the ISO.
- rr_symlink_name - The Rock Ridge name of the symlink itself on the ISO.
- rr_path - The path that the symlink points to on the Rock Ridge part
- of the ISO.
- joliet_path - The Joliet path of the symlink (if this ISO has Joliet).
- udf_symlink_path - The UDF path of the symlink itself on the ISO.
- udf_target - The UDF name of the entry on the ISO that the symlink
- points to.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- # There are actually quite a few combinations and rules to think about
- # here. Rules:
- #
- # 1. The ISO must have Rock Ridge or UDF (or both).
- # 2. If the rr_symlink_name or rr_path are supplied, the ISO must be a
- # Rock Ridge one.
- # 3. If the rr_symlink_name or rr_path are supplied, both must be
- # supplied.
- # 4. If udf_symlink_path or udf_target are supplied, the ISO must be a
- # UDF one.
- # 5. If udf_symlink_path or udf_target are supplied, both must be
- # supplied.
- # 6. For backwards compatibility reasons, symlink_path is a little
- # weird. If making a Rock Ridge symlink it is required. If making
- # a UDF symlink, it is optional.
- # 7. If a symlink_path is specified, at least one of the pair of
- # (rr_symlink_name, rr_path) or (udf_symlink_path, udf_target) must
- # be supplied.
- # 8. All arguments cannot be None.
- # 9. joliet_path is the optional path on the Joliet filesystem; if it
- # is provided, the ISO must be a Joliet one.
-
- if not self.rock_ridge and not self._has_udf:
- # Rule 1
- raise pycdlibexception.PyCdlibInvalidInput('Can only add a symlink to a Rock Ridge or UDF ISO')
-
- if (rr_symlink_name is not None or rr_path is not None) and not self.rock_ridge:
- # Rule 2
- raise pycdlibexception.PyCdlibInvalidInput('A Rock Ridge symlink can only be created on a Rock Ridge ISO')
-
- rr_tuple = (rr_symlink_name, rr_path)
- rr_vars_provided = len([x for x in rr_tuple if x is not None])
- if rr_vars_provided > 0 and rr_vars_provided != 2:
- # Rule 3
- raise pycdlibexception.PyCdlibInvalidInput("Both of 'rr_symlink_name' and 'rr_path' must be provided for a Rock Ridge symlink")
-
- if (udf_symlink_path is not None or udf_target is not None) and not self._has_udf:
- # Rule 4
- raise pycdlibexception.PyCdlibInvalidInput('A UDF symlink can only be created on a UDF ISO')
-
- udf_tuple = (udf_symlink_path, udf_target)
- udf_vars_provided = len([x for x in udf_tuple if x is not None])
- if udf_vars_provided > 0 and udf_vars_provided != 2:
- # Rule 5
- raise pycdlibexception.PyCdlibInvalidInput("Both of 'udf_symlink_path' and 'udf_target' must be provided for a UDF symlink")
-
- if rr_symlink_name is not None and symlink_path is None:
- # Rule 6
- raise pycdlibexception.PyCdlibInvalidInput("When making a Rock Ridge symlink 'symlink_path' is required")
-
- if symlink_path is not None and rr_vars_provided == 0 and udf_vars_provided == 0:
- # Rule 7
- raise pycdlibexception.PyCdlibInvalidInput('Either a Rock Ridge or a UDF symlink must be specified')
-
- all_vars_provided = len([x for x in ((symlink_path,) + rr_tuple + udf_tuple) if x is not None])
- if all_vars_provided == 0:
- # Rule 8
- raise pycdlibexception.PyCdlibInvalidInput('Either a Rock Ridge or a UDF symlink must be specified')
-
- if joliet_path is not None and self.joliet_vd is None:
- # Rule 9
- raise pycdlibexception.PyCdlibInvalidInput('A Joliet path can only be specified for a Joliet ISO')
-
- # Checks complete, we can go on to make the symlink.
-
- num_bytes_to_add = 0
-
- if symlink_path is not None:
- symlink_path_bytes = utils.normpath(symlink_path)
- (name, parent) = self._iso_name_and_parent_from_path(symlink_path_bytes)
-
- rec = dr.DirectoryRecord()
-
- if rr_symlink_name is not None and rr_path is not None:
- # We specifically do *not* normalize rr_path here, since that
- # potentially changes the meaning of what the user wanted.
-
- rr_symlink_name_bytes = rr_symlink_name.encode('utf-8')
- rec.new_symlink(self.pvd, name, parent, rr_path.encode('utf-8'),
- self.pvd.sequence_number(), self.rock_ridge,
- rr_symlink_name_bytes, self.xa, time.time())
- num_bytes_to_add += self._add_child_to_dr(rec)
-
- num_bytes_to_add += self._update_rr_ce_entry(rec)
-
- if udf_symlink_path is not None and udf_target is not None:
- # If we aren't making a Rock Ridge symlink at the same time, we need
- # to add a new zero-byte file to the ISO.
- if rr_path is None:
- tmp_joliet_path = joliet_path
- if tmp_joliet_path is None:
- tmp_joliet_path = ''
- num_bytes_to_add += self._add_fp(None, 0, False, symlink_path,
- '', tmp_joliet_path, '', None,
- False)
-
- udf_symlink_path_bytes = utils.normpath(udf_symlink_path)
-
- # We specifically do *not* normalize udf_target here, since that
- # potentially changes the meaning of what the user wanted.
-
- (udf_name, udf_parent) = self._udf_name_and_parent_from_path(udf_symlink_path_bytes)
- file_ident = udfmod.UDFFileIdentifierDescriptor()
- file_ident.new(False, False, udf_name, udf_parent)
- num_new_extents = udf_parent.add_file_ident_desc(file_ident, self.logical_block_size)
- num_bytes_to_add += num_new_extents * self.logical_block_size
-
- # Generate the bytearry representing the symlink.
- symlink_bytearray = udfmod.symlink_to_bytes(udf_target)
-
- file_entry = udfmod.UDFFileEntry()
- file_entry.new(len(symlink_bytearray), 'symlink', udf_parent,
- self.logical_block_size)
- file_ident.file_entry = file_entry
- file_entry.file_ident = file_ident
- num_bytes_to_add += self.logical_block_size
- num_bytes_to_add += file_entry.info_len
-
- # The inode for the symlink array.
- ino = inode.Inode()
- ino.new(len(symlink_bytearray), io.BytesIO(symlink_bytearray), False, 0)
- ino.linked_records.append((file_entry, False))
- ino.num_udf += 1
- file_entry.inode = ino
- self.inodes.append(ino)
-
- if self.udf_logical_volume_integrity is not None:
- self.udf_logical_volume_integrity.logical_volume_impl_use.num_files += 1
-
- # Note that we explicitly do *not* link this record to the ISO9660
- # record; that's because there is no way to correlate them during
- # parse time. Instead, we treat them as individual entries, which
- # has the knock-on effect of requiring two operations to remove;
- # rm_file() to remove the ISO9660 record, and rm_hard_link() to
- # remove the UDF record.
-
- if joliet_path is not None:
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInternalError('Tried to add a Joliet path to a non-Joliet ISO')
- joliet_path_bytes = self._normalize_joliet_path(joliet_path)
- (joliet_name, joliet_parent) = self._joliet_name_and_parent_from_path(joliet_path_bytes)
-
- # Add in a "fake" symlink entry for Joliet.
- joliet_rec = dr.DirectoryRecord()
- joliet_rec.new_file(self.joliet_vd, 0, joliet_name, joliet_parent,
- self.joliet_vd.sequence_number(), '', b'',
- self.xa, -1, time.time())
- num_bytes_to_add += self._add_child_to_dr(joliet_rec)
-
- self._finish_add(0, num_bytes_to_add)
-
- def list_dir(self, iso_path, joliet=False):
- # type: (str, bool) -> Generator
- """
- (deprecated) Generate a list of all of the file/directory objects in the
- specified location on the ISO. It is recommended to use the
- 'list_children' API instead.
-
- Parameters:
- iso_path - The path on the ISO to look up information for.
- joliet - Whether to look for the path in the Joliet portion of the ISO.
- Yields:
- Children of this path.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- use_rr = False
- if joliet:
- rec = self._get_joliet_entry(self._normalize_joliet_path(iso_path))
- else:
- normpath = utils.normpath(iso_path)
- try_rr = False
- try:
- rec = self._get_iso_entry(normpath)
- except pycdlibexception.PyCdlibInvalidInput:
- try_rr = True
-
- if try_rr:
- rec = self._get_rr_entry(normpath)
- use_rr = True
-
- for c in _yield_children(rec, use_rr): # pylint: disable=use-yield-from
- yield c
-
- def list_children(self, **kwargs):
- # type: (str) -> Generator
- """
- Generate a list of all of the file/directory objects in the
- specified location on the ISO.
-
- Parameters:
- iso_path - The absolute path on the ISO to list the children for.
- rr_path - The absolute Rock Ridge path on the ISO to list the children for.
- joliet_path - The absolute Joliet path on the ISO to list the children for.
- udf_path - The absolute UDF path on the ISO to list the children for.
- encoding - The string encoding used for the path; defaults to 'utf-8' or 'utf-16_be'
- Yields:
- Children of this path.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_paths = 0
- for key, value in kwargs.items():
- if key in ('joliet_path', 'rr_path', 'iso_path', 'udf_path'):
- if value is not None:
- num_paths += 1
- elif key in ('encoding'):
- continue
- else:
- raise pycdlibexception.PyCdlibInvalidInput("Invalid keyword, must be one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Must specify one, and only one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if 'udf_path' in kwargs:
- udf_rec = self._get_udf_entry(kwargs['udf_path'])
-
- if not udf_rec.is_dir():
- raise pycdlibexception.PyCdlibInvalidInput('UDF File Entry is not a directory!')
-
- for fi_desc in udf_rec.fi_descs:
- yield fi_desc.file_entry
- else:
- use_rr = False
- if 'joliet_path' in kwargs:
- kwargs['encoding'] = kwargs.get('encoding') or 'utf-16_be'
- rec = self._get_joliet_entry(self._normalize_joliet_path(kwargs['joliet_path']), kwargs['encoding'])
- elif 'rr_path' in kwargs:
- kwargs['encoding'] = kwargs.get('encoding') or 'utf-8'
- rec = self._get_rr_entry(utils.normpath(kwargs['rr_path']), kwargs['encoding'])
- use_rr = True
- else:
- kwargs['encoding'] = kwargs.get('encoding') or 'utf-8'
- rec = self._get_iso_entry(utils.normpath(kwargs['iso_path']), kwargs['encoding'])
-
- for c in _yield_children(rec, use_rr): # pylint: disable=use-yield-from
- yield c
-
- def get_entry(self, iso_path, joliet=False):
- # type: (str, bool) -> dr.DirectoryRecord
- """
- (deprecated) Get the directory record for a particular path. It is
- recommended to use the 'get_record' API instead.
-
- Parameters:
- iso_path - The path on the ISO to look up information for.
- joliet - Whether to look for the path in the Joliet portion of the ISO.
- Returns:
- A dr.DirectoryRecord object representing the path.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if joliet:
- return self._get_joliet_entry(self._normalize_joliet_path(iso_path))
- return self._get_iso_entry(utils.normpath(iso_path))
-
- def get_record(self, **kwargs):
- # type: (str) -> Union[dr.DirectoryRecord, udfmod.UDFFileEntry]
- """
- Get the directory record for a particular path.
-
- Parameters:
- iso_path - The absolute path on the ISO9660 filesystem to get the
- record for.
- rr_path - The absolute path on the Rock Ridge filesystem to get the
- record for.
- joliet_path - The absolute path on the Joliet filesystem to get the
- record for.
- udf_path - The absolute path on the UDF filesystem to get the record
- for.
- Returns:
- An object that represents the path. This may be a dr.DirectoryRecord
- object (in the cases of iso_path, rr_path, or joliet_path), or a
- udf.UDFFileEntry object (in the case of udf_path).
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_paths = 0
- for key, value in kwargs.items():
- if key in ('joliet_path', 'rr_path', 'iso_path', 'udf_path'):
- if value is not None:
- num_paths += 1
- else:
- raise pycdlibexception.PyCdlibInvalidInput("Invalid keyword, must be one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Must specify one, and only one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if 'joliet_path' in kwargs:
- return self._get_joliet_entry(self._normalize_joliet_path(kwargs['joliet_path']))
- if 'rr_path' in kwargs:
- return self._get_rr_entry(utils.normpath(kwargs['rr_path']))
- if 'udf_path' in kwargs:
- return self._get_udf_entry(kwargs['udf_path'])
- return self._get_iso_entry(utils.normpath(kwargs['iso_path']))
-
- def add_isohybrid(self, part_entry=1, mbr_id=None, part_offset=0,
- geometry_sectors=32, geometry_heads=64, part_type=None,
- mac=False, efi=None):
- # type: (int, Optional[int], int, int, int, Optional[int], bool, Optional[bool]) -> None
- """
- Make an ISO a 'hybrid', which means that it can be booted either from a
- CD or from more traditional media (like a USB stick). This requires
- that the ISO already have El Torito, and will use the El Torito boot
- file as a bootable image. That image must contain a certain signature
- in order to work as a hybrid (if using syslinux, this generally means
- the isohdpfx.bin files).
-
- Parameters:
- part_entry - The partition entry to use; one by default.
- mbr_id - The mbr_id to use. If set to None (the default), a random one
- will be generated.
- part_offset - The partition offset to use; zero by default.
- geometry_sectors - The number of sectors to assign; thirty-two by
- default.
- geometry_heads - The number of heads to assign; sixty-four by default.
- part_type - The partition type to assign; twenty-three by default, but
- will automatically be set to 0 if mac or efi are True.
- mac - Add support for Mac; False by default.
- efi - Add support for EFI; False by default, but will automatically
- be set to True if mac is True.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if self.eltorito_boot_catalog is None:
- raise pycdlibexception.PyCdlibInvalidInput('The ISO must have an El Torito Boot Record to add isohybrid support')
-
- if self.eltorito_boot_catalog.initial_entry.sector_count != 4:
- raise pycdlibexception.PyCdlibInvalidInput('El Torito Boot Catalog sector count must be 4 (was actually 0x%x)' % (self.eltorito_boot_catalog.initial_entry.sector_count))
-
- if efi is not None:
- if not efi and mac:
- raise pycdlibexception.PyCdlibInvalidInput('If mac is True, efi must also be True')
- else:
- efi = False
- if mac:
- efi = True
-
- if part_type is None:
- part_type = 0x17
- if mac or efi:
- part_type = 0
-
- # Check that the eltorito boot file contains the appropriate
- # signature (offset 0x40, '\xFB\xC0\x78\x70').
- with inode.InodeOpenData(self.eltorito_boot_catalog.initial_entry.inode, self.logical_block_size) as (data_fp, data_len_unused):
- data_fp.seek(0x40, os.SEEK_CUR)
- signature = data_fp.read(4)
-
- if signature != b'\xfb\xc0\x78\x70':
- raise pycdlibexception.PyCdlibInvalidInput('Invalid signature on boot file for iso hybrid')
-
- self.isohybrid_mbr = isohybrid.IsoHybrid()
- self.isohybrid_mbr.new(efi, mac, part_entry, mbr_id, part_offset,
- geometry_sectors, geometry_heads, part_type)
-
- def rm_isohybrid(self):
- # type: () -> None
- """
- Remove the 'hybridization' of an ISO, making it a traditional ISO again.
- This means the ISO will no longer be able to be copied and booted off
- of traditional media (like USB sticks).
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- self.isohybrid_mbr = None
-
- def full_path_from_dirrecord(self, rec, rockridge=False, user_encoding=''):
- # type: (Union[dr.DirectoryRecord, udfmod.UDFFileEntry], bool, str) -> str
- """
- Get the absolute path of a directory record.
-
- Parameters:
- rec - The directory record to get the full path for.
- rockridge - Whether to get the rock ridge full path.
- user_encoding - The string encoding used for the path as determined by the user.
- Returns:
- A string representing the absolute path to the file on the ISO.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- names = [] # type: List[str]
- if isinstance(rec, dr.DirectoryRecord):
- encoding = 'utf-8'
- if self.joliet_vd is not None and id(rec.vd) == id(self.joliet_vd):
- encoding = 'utf-16_be'
-
- if user_encoding:
- encoding = user_encoding
-
- # A root entry has no Rock Ridge entry, even on a Rock Ridge ISO.
- # Always return / here.
- if rec.is_root:
- return '/'
-
- if rockridge and rec.rock_ridge is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot generate a Rock Ridge path on a non-Rock Ridge ISO')
-
- dr_rec = rec # type: Optional[dr.DirectoryRecord]
- while dr_rec is not None:
- next_rec = None
- if dr_rec.is_root:
- name = b''
- else:
- if rockridge:
- if dr_rec.rock_ridge is not None:
- for child in dr_rec.children:
- if child.is_dotdot():
- if child.rock_ridge is not None and child.rock_ridge.parent_link_record_exists():
- next_rec = child.rock_ridge.parent_link
- break
- name = dr_rec.rock_ridge.name()
- else:
- name = dr_rec.file_identifier()
- else:
- name = dr_rec.file_identifier()
-
- names.insert(0, name.decode(encoding))
- if next_rec is not None:
- dr_rec = next_rec
- else:
- dr_rec = dr_rec.parent
- else:
- if rec.parent is None:
- return '/'
- if rec.file_ident is not None:
- encoding = rec.file_ident.encoding
- else:
- encoding = 'utf-8'
- if user_encoding:
- encoding = user_encoding
- udf_rec = rec # type: Optional[udfmod.UDFFileEntry]
- while udf_rec is not None:
- ident = udf_rec.file_identifier()
- if ident == b'/':
- name = b''
- else:
- name = ident
- names.insert(0, name.decode(encoding))
- udf_rec = udf_rec.parent
-
- # Return the encoded version.
- return '/'.join(names)
-
- def duplicate_pvd(self):
- # type: () -> None
- """
- Add a duplicate PVD to the ISO. This is a mostly useless feature
- allowed by Ecma-119 to have duplicate PVDs to avoid possible corruption.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- pvd = headervd.PrimaryOrSupplementaryVD(headervd.VOLUME_DESCRIPTOR_TYPE_PRIMARY)
- pvd.copy(self.pvd)
- self.pvds.append(pvd)
-
- self._finish_add(self.logical_block_size, 0)
-
- def set_hidden(self, iso_path=None, rr_path=None, joliet_path=None):
- # type: (Optional[str], Optional[str], Optional[str]) -> None
- """
- Set the ISO9660 hidden attribute on a file or directory. This will
- cause the file or directory not to show up when listing entries on the
- ISO. Exactly one of iso_path, rr_path, or joliet_path must be specified.
-
- Parameters:
- iso_path - The path on the ISO to set the hidden bit on.
- rr_path - The Rock Ridge path on the ISO to set the hidden bit on.
- joliet_path - The Joliet path on the ISO to set the hidden bit on.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if len([x for x in (iso_path, rr_path, joliet_path) if x is not None]) != 1:
- raise pycdlibexception.PyCdlibInvalidInput('Must provide exactly one of iso_path, rr_path, or joliet_path')
-
- if iso_path is not None:
- rec = self._find_iso_record(utils.normpath(iso_path))
- elif rr_path is not None:
- rec = self._find_rr_record(utils.normpath(rr_path))
- elif joliet_path is not None:
- joliet_path_bytes = self._normalize_joliet_path(joliet_path)
- rec = self._find_joliet_record(joliet_path_bytes)
- else:
- raise pycdlibexception.PyCdlibInvalidInput('Must provide exactly one of iso_path, rr_path, or joliet_path')
-
- rec.change_existence(True)
-
- def clear_hidden(self, iso_path=None, rr_path=None, joliet_path=None):
- # type: (Optional[str], Optional[str], Optional[str]) -> None
- """
- Clear the ISO9660 hidden attribute on a file or directory. This will
- cause the file or directory to show up when listing entries on the ISO.
- Exactly one of iso_path, rr_path, or joliet_path must be specified.
-
- Parameters:
- iso_path - The path on the ISO to clear the hidden bit from.
- rr_path - The Rock Ridge path on the ISO to clear the hidden bit from.
- joliet_path - The Joliet path on the ISO to clear the hidden bit from.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if len([x for x in (iso_path, rr_path, joliet_path) if x is not None]) != 1:
- raise pycdlibexception.PyCdlibInvalidInput('Must provide exactly one of iso_path, rr_path, or joliet_path')
-
- if iso_path is not None:
- rec = self._find_iso_record(utils.normpath(iso_path))
- elif rr_path is not None:
- rec = self._find_rr_record(utils.normpath(rr_path))
- elif joliet_path is not None:
- joliet_path_bytes = self._normalize_joliet_path(joliet_path)
- rec = self._find_joliet_record(joliet_path_bytes)
- else:
- raise pycdlibexception.PyCdlibInvalidInput('Must provide exactly one of iso_path, rr_path, or joliet_path')
-
- rec.change_existence(False)
-
- def force_consistency(self):
- # type: () -> None
- """
- Make sure the ISO object is fully consistent. PyCdlib typically delays
- doing work until it is necessary, and this detail is usually hidden
- from users. However, there are times that a user may want a fully
- consistent view of the ISO without calling one of the methods that
- forces consistency. This method allows the user to force a consistent
- view of this object.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- self._reshuffle_extents()
-
- def set_relocated_name(self, name, rr_name):
- # type: (str, str) -> None
- """
- Set the name of the relocated directory on a Rock Ridge ISO. The ISO
- must be a Rock Ridge one, and must not have previously had the relocated
- name set.
-
- Parameters:
- name - The name for a relocated directory.
- rr_name - The Rock Ridge name for a relocated directory.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Can only set the relocated name on a Rock Ridge ISO')
-
- encoded_name = name.encode('utf-8')
- encoded_rr_name = rr_name.encode('utf-8')
- if self._rr_moved_name is not None:
- if self._rr_moved_name == encoded_name and self._rr_moved_rr_name == encoded_rr_name:
- return
- raise pycdlibexception.PyCdlibInvalidInput('Changing the existing rr_moved name is not allowed')
-
- _check_iso9660_directory(encoded_name, self.interchange_level)
- self._rr_moved_name = encoded_name
- self._rr_moved_rr_name = encoded_rr_name
-
- def walk(self, **kwargs):
- # type: (str) -> Generator
- """
- Walk the entries on the ISO, starting at the given path. One, and only
- one, of iso_path, rr_path, joliet_path, and udf_path is allowed.
- Similar to os.walk(), yield a 3-tuple of (path-to-here, dirlist, filelist)
- for each directory level.
-
- Parameters:
- iso_path - The absolute ISO path to the starting entry on the ISO.
- rr_path - The absolute Rock Ridge path to the starting entry on the ISO.
- joliet_path - The absolute Joliet path to the starting entry on the ISO.
- udf_path - The absolute UDF path to the starting entry on the ISO.
- encoding - The encoding to use for returned strings.
- Yields:
- 3-tuples of (path-to-here, dirlist, filelist)
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_paths = 0
- user_encoding = ''
- for key, value in kwargs.items():
- if key in ('joliet_path', 'rr_path', 'iso_path', 'udf_path') and value is not None:
- num_paths += 1
- elif key == 'encoding' and value:
- user_encoding = value
- else:
- raise pycdlibexception.PyCdlibInvalidInput("Invalid keyword, must be one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Must specify one, and only one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- rec = None # type: Optional[Union[dr.DirectoryRecord, udfmod.UDFFileEntry]]
- if 'joliet_path' in kwargs:
- joliet_path = self._normalize_joliet_path(kwargs['joliet_path'])
- rec = self._find_joliet_record(joliet_path)
- path_type = 'joliet_path'
- default_encoding = 'utf-16_be'
- elif 'udf_path' in kwargs:
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
- (ident_unused, rec) = self._find_udf_record(utils.normpath(kwargs['udf_path']))
- if rec is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot get entry for empty UDF File Entry')
- path_type = 'udf_path'
- default_encoding = ''
- elif 'rr_path' in kwargs:
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a rr_path from a non-Rock Ridge ISO')
- rec = self._find_rr_record(utils.normpath(kwargs['rr_path']))
- path_type = 'rr_path'
- default_encoding = 'utf-8'
- else:
- rec = self._find_iso_record(utils.normpath(kwargs['iso_path']))
- path_type = 'iso_path'
- default_encoding = 'utf-8'
-
- dirs = collections.deque([rec])
- while dirs:
- dir_record = dirs.popleft()
-
- relpath = self.full_path_from_dirrecord(dir_record, rockridge=path_type == 'rr_path',
- user_encoding=user_encoding)
- dirlist = []
- filelist = []
- dirdict = {}
-
- for child in reversed(list(self.list_children(**{path_type: relpath, 'encoding': user_encoding or default_encoding}))):
- if child is None or child.is_dot() or child.is_dotdot():
- continue
-
- if user_encoding != '':
- encoding = user_encoding
- elif isinstance(child, udfmod.UDFFileEntry) and child.file_ident is not None:
- encoding = child.file_ident.encoding
- else:
- encoding = default_encoding or 'utf-8'
-
- if path_type == 'rr_path':
- name = child.rock_ridge.name()
- else:
- name = child.file_identifier()
-
- encoded = name.decode(encoding)
-
- if child.is_dir():
- dirlist.append(encoded)
- dirdict[encoded] = child
- else:
- filelist.append(encoded)
-
- yield relpath, dirlist, filelist
-
- # We allow the user to modify dirlist along the way, so we
- # add the children to dirs *after* yield returns.
- for name in dirlist:
- dirs.appendleft(dirdict[name])
-
- def open_file_from_iso(self, **kwargs):
- # type: (str) -> pycdlibio.PyCdlibIO
- """
- Open a file for reading in a context manager. This allows the user to
- operate on the file in user-defined chunks (utilizing the read() method
- of the returned context manager).
-
- Parameters:
- iso_path - The absolute ISO path to the file on the ISO.
- rr_path - The absolute Rock Ridge path to the file on the ISO.
- joliet_path - The absolute Joliet path to the file on the ISO.
- udf_path - The absolute UDF path to the file on the ISO.
- Returns:
- A PyCdlibIO object allowing access to the file.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_paths = 0
- rec = None # type: Optional[Union[dr.DirectoryRecord, udfmod.UDFFileEntry]]
- for key, value in kwargs.items():
- if key in ('joliet_path', 'rr_path', 'iso_path', 'udf_path'):
- if value is not None:
- num_paths += 1
- else:
- raise pycdlibexception.PyCdlibInvalidInput("Invalid keyword, must be one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Must specify one, and only one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if 'joliet_path' in kwargs:
- joliet_path = self._normalize_joliet_path(kwargs['joliet_path'])
- rec = self._find_joliet_record(joliet_path)
- elif 'udf_path' in kwargs:
- if self.udf_root is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only specify a UDF path for a UDF ISO')
- (ident_unused, rec) = self._find_udf_record(utils.normpath(kwargs['udf_path']))
- if rec is None:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot get entry for empty UDF File Entry')
- elif 'rr_path' in kwargs:
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a rr_path from a non-Rock Ridge ISO')
- rec = self._find_rr_record(utils.normpath(kwargs['rr_path']))
- else:
- rec = self._find_iso_record(utils.normpath(kwargs['iso_path']))
-
- if not rec.is_file():
- raise pycdlibexception.PyCdlibInvalidInput('Path to open must be a file')
-
- if rec.inode is None:
- raise pycdlibexception.PyCdlibInvalidInput('File has no data')
-
- return pycdlibio.PyCdlibIO(rec.inode, self.logical_block_size)
-
- def has_rock_ridge(self):
- # type: () -> bool
- """
- Returns whether this ISO has Rock Ridge extensions.
-
- Parameters:
- None.
- Returns:
- True if this ISO has Rock Ridge extensions, False otherwise.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- return self.rock_ridge != ''
-
- def has_joliet(self):
- # type: () -> bool
- """
- Returns whether this ISO has Joliet extensions.
-
- Parameters:
- None.
- Returns:
- True if this ISO has Joliet, False otherwise.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- return self.joliet_vd is not None
-
- def has_udf(self):
- # type: () -> bool
- """
- Returns whether this ISO has UDF extensions.
-
- Parameters:
- None.
- Returns:
- True if this ISO has UDF, False otherwise.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- return self._has_udf
-
- def get_iso9660_facade(self):
- # type: () -> facade.PyCdlibISO9660
- """
- Return a 'facade' that simplifies some of the complexities of the
- PyCdlib class, while giving up some of the full power. This facade
- only allows manipulation of the ISO9660 portions of the ISO.
-
- Parameters:
- None.
- Returns:
- A PyCdlibISO9660 object that can be used to interact with the ISO.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- return facade.PyCdlibISO9660(self)
-
- def get_joliet_facade(self):
- # type: () -> facade.PyCdlibJoliet
- """
- Return a 'facade' that simplifies some of the complexities of the
- PyCdlib class, while giving up some of the full power. This facade
- only allows manipulation of the Joliet portions of the ISO.
-
- Parameters:
- None.
- Returns:
- A PyCdlibJoliet object that can be used to interact with the ISO.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- if self.joliet_vd is None:
- raise pycdlibexception.PyCdlibInvalidInput('Can only get a Joliet facade for a Joliet ISO')
-
- return facade.PyCdlibJoliet(self)
-
- def get_rock_ridge_facade(self):
- # type: () -> facade.PyCdlibRockRidge
- """
- Return a 'facade' that simplifies some of the complexities of the
- PyCdlib class, while giving up some of the full power. This facade
- only allows manipulation of the Rock Ridge portions of the ISO.
-
- Parameters:
- None.
- Returns:
- A PyCdlibRockRidge object that can be used to interact with the ISO.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- if self.rock_ridge == '':
- raise pycdlibexception.PyCdlibInvalidInput('Can only get a Rock Ridge facade for a Rock Ridge ISO')
-
- return facade.PyCdlibRockRidge(self)
-
- def get_udf_facade(self):
- # type: () -> facade.PyCdlibUDF
- """
- Return a 'facade' that simplifies some of the complexities of the
- PyCdlib class, while giving up some of the full power. This facade
- only allows manipulation of the UDF portions of the ISO.
-
- Parameters:
- None.
- Returns:
- A PyCdlibUDF object that can be used to interact with the ISO.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
- if not self._has_udf:
- raise pycdlibexception.PyCdlibInvalidInput('Can only get a UDF facade for a UDF ISO')
-
- return facade.PyCdlibUDF(self)
-
- def file_mode(self, **kwargs):
- # type: (str) -> Optional[int]
- """
- Get the POSIX file mode of the file if is a Rock Ridge file.
-
- Parameters:
- iso_path - The absolute ISO path to the file on the ISO.
- rr_path - The absolute Rock Ridge path to the file on the ISO.
- joliet_path - The absolute Joliet path to the file on the ISO.
- udf_path - The absolute UDF path to the file on the ISO.
- Returns:
- An integer representing the POSIX file mode of the file if it is Rock
- Ridge, or None otherwise.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- num_paths = 0
- for key, value in kwargs.items():
- if key in ('joliet_path', 'rr_path', 'iso_path', 'udf_path'):
- if value is not None:
- num_paths += 1
- else:
- raise pycdlibexception.PyCdlibInvalidInput("Invalid keyword, must be one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- if num_paths != 1:
- raise pycdlibexception.PyCdlibInvalidInput("Must specify one, and only one of 'iso_path', 'rr_path', 'joliet_path', or 'udf_path'")
-
- file_mode = None
- if 'rr_path' in kwargs:
- if not self.rock_ridge:
- raise pycdlibexception.PyCdlibInvalidInput('Cannot fetch a rr_path from a non-Rock Ridge ISO')
- rec = self._find_rr_record(utils.normpath(kwargs['rr_path']))
- if rec.rock_ridge is not None:
- if rec.rock_ridge.dr_entries.px_record is not None or rec.rock_ridge.ce_entries.px_record is not None:
- file_mode = rec.rock_ridge.get_file_mode()
-
- # Neither Joliet nor ISO know the file_mode, and we don't support setting
- # the file mode for UDF, so just return None in those cases
-
- return file_mode
-
- def close(self):
- # type: () -> None
- """
- Close the PyCdlib object, and re-initialize the object to the defaults.
- The object can then be re-used for manipulation of another ISO.
-
- Parameters:
- None.
- Returns:
- Nothing.
- """
- if not self._initialized:
- raise pycdlibexception.PyCdlibInvalidInput('This object is not initialized; call either open() or new() to create an ISO')
-
- if self._managing_fp:
- self._cdfp.close()
-
- self._initialize()
More information about the Scummvm-git-logs
mailing list