Module refinery.lib.iso
Library for parsing ISO 9660 and UDF disk images.
Expand source code Browse git
"""
Library for parsing ISO 9660 and UDF disk images.
"""
from __future__ import annotations
import datetime
import enum
from typing import TYPE_CHECKING
from refinery.lib.iso.raw import FIRST_VOLUME_DESCRIPTOR_SECTOR, SectorLayout
from refinery.lib.types import buf
if TYPE_CHECKING:
from refinery.lib.iso.iso9660 import ISORef
from refinery.lib.iso.udf import UDFRef
VOLUME_STRUCTURE_IDENTIFIERS = frozenset((
B'BEA01',
B'BOOT2',
B'CD001',
B'CDW02',
B'NSR02',
B'NSR03',
B'TEA01',
))
def is_disc_image(data: buf) -> bool:
"""
Check whether one of the first three sectors of the volume recognition sequence holds a volume
structure descriptor. ISO 9660 volume descriptors carry the identifier `CD001`, and a UDF
volume is announced by the descriptors `BEA01`, `NSR02` or `NSR03`, and `TEA01`.
"""
view = memoryview(data)
layout = SectorLayout.detect(view)
for sector in range(FIRST_VOLUME_DESCRIPTOR_SECTOR, FIRST_VOLUME_DESCRIPTOR_SECTOR + 3):
start = layout.position(sector) + 1
if bytes(view[start:start + 5]) in VOLUME_STRUCTURE_IDENTIFIERS:
return True
return False
class FileSystemType(enum.Enum):
AUTO = 'auto'
ISO = 'iso'
JOLIET = 'joliet'
RR = 'rr'
UDF = 'udf'
class ISOFile:
"""
Represents a single file entry within an ISO image.
"""
__slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key')
def __init__(
self,
path: str,
size: int,
date: datetime.datetime | None,
is_dir: bool,
extents: list[tuple[int, int]],
key: tuple[FileSystemType, ISORef | UDFRef],
inline: bytes | memoryview | None = None,
):
self.path = path
self.size = size
self.date = date
self.is_dir = is_dir
self.extents = extents
self._inline = inline
self.key = key
class ISOArchive:
"""
Unified ISO/UDF archive reader. Tries UDF first, falls back to ISO 9660. Raw CD images are
reduced to the user data of their sectors first, see `refinery.lib.iso.raw.SectorLayout`.
"""
def __init__(self, data):
from refinery.lib.iso.udf import ANCHOR_SECTOR, UDFArchive, _verify_tag
self.sector_layout = layout = SectorLayout.detect(data)
if layout.raw:
data = layout.user_data(data)
self._data = data
self._iso = None
self._udf = None
self._type = FileSystemType.ISO
udf_found = False
dv = memoryview(self._data)
for ss in (2048, 512, 4096):
anchor_pos = ANCHOR_SECTOR * ss
if anchor_pos + 16 <= len(dv):
tag_id = _verify_tag(dv, anchor_pos)
if tag_id == 2:
udf_found = True
break
if udf_found:
udf = UDFArchive()
try:
udf.open(self._data)
except Exception:
udf = None
if udf is not None and udf.refs:
self._udf = udf
self._type = FileSystemType.UDF
return
from refinery.lib.iso.iso9660 import ISO9660Archive
iso = ISO9660Archive()
iso.open(self._data)
self._iso = iso
self._type = iso.filesystem_type
@property
def filesystem_type(self) -> str:
return self._type.value
def select_filesystem(self, fs: FileSystemType) -> bool:
"""
Select the given file system and return whether the image contains it. Otherwise, the
previous selection remains, except that requesting Rock Ridge while ISO 9660 is selected
yields the plain ISO 9660 names when the primary volume lacks these extensions.
"""
if fs is FileSystemType.UDF:
if self._udf is None:
from refinery.lib.iso.udf import UDFArchive
udf = UDFArchive()
try:
udf.open(self._data)
except Exception:
return False
if not udf.refs:
return False
self._udf = udf
self._type = FileSystemType.UDF
return True
if fs not in (FileSystemType.JOLIET, FileSystemType.RR, FileSystemType.ISO):
return False
if (iso := self._iso) is None:
from refinery.lib.iso.iso9660 import ISO9660Archive
self._iso = iso = ISO9660Archive()
iso.open(self._data)
selected = iso.select_filesystem(fs)
if selected or self._type is not FileSystemType.UDF:
self._type = iso.filesystem_type
return selected
def entries(self):
if self._type is FileSystemType.UDF and self._udf is not None:
for ref in self._udf.entries():
yield ISOFile(
path=ref.path,
size=ref.total_size,
date=ref.date,
is_dir=ref.is_dir,
extents=ref.extents,
inline=ref.inline_data,
key=(FileSystemType.UDF, ref),
)
elif self._iso is not None:
for ref in self._iso.entries():
yield ISOFile(
path=ref.path,
size=ref.total_size,
date=ref.date,
is_dir=ref.is_dir,
extents=ref.extents,
key=(FileSystemType.ISO, ref),
)
def extract(self, entry: ISOFile) -> bytes | bytearray:
from refinery.lib.iso.iso9660 import ISORef
from refinery.lib.iso.udf import UDFRef
backend_id, ref = entry.key
if isinstance(ref, ISORef) and (iso := self._iso):
if backend_id != FileSystemType.ISO:
raise ValueError(F'File System Inconsistency; expected ISO, got {backend_id.name}.')
return iso.extract(ref)
if isinstance(ref, UDFRef) and (udf := self._udf):
if backend_id != FileSystemType.UDF:
raise ValueError(F'File System Inconsistency; expected UDF, got {backend_id.name}.')
return udf.extract(ref)
return b''
Sub-modules
refinery.lib.iso.eltorito-
El Torito boot catalog parser for ISO 9660 images.
refinery.lib.iso.iso9660-
ISO 9660 filesystem parser ported from 7zip's Archive/Iso/ implementation.
refinery.lib.iso.raw-
Physical sector layouts of optical disc images. A raw CD image stores each sector with its sync pattern, header, and error correction data, whereas …
refinery.lib.iso.udf-
UDF (ECMA-167) filesystem parser ported from 7zip's Archive/Udf/ implementation.
Functions
def is_disc_image(data)-
Check whether one of the first three sectors of the volume recognition sequence holds a volume structure descriptor. ISO 9660 volume descriptors carry the identifier
CD001, and a UDF volume is announced by the descriptorsBEA01,NSR02orNSR03, andTEA01.Expand source code Browse git
def is_disc_image(data: buf) -> bool: """ Check whether one of the first three sectors of the volume recognition sequence holds a volume structure descriptor. ISO 9660 volume descriptors carry the identifier `CD001`, and a UDF volume is announced by the descriptors `BEA01`, `NSR02` or `NSR03`, and `TEA01`. """ view = memoryview(data) layout = SectorLayout.detect(view) for sector in range(FIRST_VOLUME_DESCRIPTOR_SECTOR, FIRST_VOLUME_DESCRIPTOR_SECTOR + 3): start = layout.position(sector) + 1 if bytes(view[start:start + 5]) in VOLUME_STRUCTURE_IDENTIFIERS: return True return False
Classes
class FileSystemType (*args, **kwds)-
Create a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3Access them by:
- attribute access:
Color.RED
- value lookup:
Color(1)
- name lookup:
Color['RED']
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
Expand source code Browse git
class FileSystemType(enum.Enum): AUTO = 'auto' ISO = 'iso' JOLIET = 'joliet' RR = 'rr' UDF = 'udf'Ancestors
- enum.Enum
Class variables
var AUTO-
The type of the None singleton.
var ISO-
The type of the None singleton.
var JOLIET-
The type of the None singleton.
var RR-
The type of the None singleton.
var UDF-
The type of the None singleton.
class ISOFile (path, size, date, is_dir, extents, key, inline=None)-
Represents a single file entry within an ISO image.
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = keyInstance variables
var date-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key var extents-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key var is_dir-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key var key-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key var path-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key var size-
Expand source code Browse git
class ISOFile: """ Represents a single file entry within an ISO image. """ __slots__ = ('path', 'size', 'date', 'is_dir', 'extents', '_inline', 'key') def __init__( self, path: str, size: int, date: datetime.datetime | None, is_dir: bool, extents: list[tuple[int, int]], key: tuple[FileSystemType, ISORef | UDFRef], inline: bytes | memoryview | None = None, ): self.path = path self.size = size self.date = date self.is_dir = is_dir self.extents = extents self._inline = inline self.key = key
class ISOArchive (data)-
Unified ISO/UDF archive reader. Tries UDF first, falls back to ISO 9660. Raw CD images are reduced to the user data of their sectors first, see
SectorLayout.Expand source code Browse git
class ISOArchive: """ Unified ISO/UDF archive reader. Tries UDF first, falls back to ISO 9660. Raw CD images are reduced to the user data of their sectors first, see `refinery.lib.iso.raw.SectorLayout`. """ def __init__(self, data): from refinery.lib.iso.udf import ANCHOR_SECTOR, UDFArchive, _verify_tag self.sector_layout = layout = SectorLayout.detect(data) if layout.raw: data = layout.user_data(data) self._data = data self._iso = None self._udf = None self._type = FileSystemType.ISO udf_found = False dv = memoryview(self._data) for ss in (2048, 512, 4096): anchor_pos = ANCHOR_SECTOR * ss if anchor_pos + 16 <= len(dv): tag_id = _verify_tag(dv, anchor_pos) if tag_id == 2: udf_found = True break if udf_found: udf = UDFArchive() try: udf.open(self._data) except Exception: udf = None if udf is not None and udf.refs: self._udf = udf self._type = FileSystemType.UDF return from refinery.lib.iso.iso9660 import ISO9660Archive iso = ISO9660Archive() iso.open(self._data) self._iso = iso self._type = iso.filesystem_type @property def filesystem_type(self) -> str: return self._type.value def select_filesystem(self, fs: FileSystemType) -> bool: """ Select the given file system and return whether the image contains it. Otherwise, the previous selection remains, except that requesting Rock Ridge while ISO 9660 is selected yields the plain ISO 9660 names when the primary volume lacks these extensions. """ if fs is FileSystemType.UDF: if self._udf is None: from refinery.lib.iso.udf import UDFArchive udf = UDFArchive() try: udf.open(self._data) except Exception: return False if not udf.refs: return False self._udf = udf self._type = FileSystemType.UDF return True if fs not in (FileSystemType.JOLIET, FileSystemType.RR, FileSystemType.ISO): return False if (iso := self._iso) is None: from refinery.lib.iso.iso9660 import ISO9660Archive self._iso = iso = ISO9660Archive() iso.open(self._data) selected = iso.select_filesystem(fs) if selected or self._type is not FileSystemType.UDF: self._type = iso.filesystem_type return selected def entries(self): if self._type is FileSystemType.UDF and self._udf is not None: for ref in self._udf.entries(): yield ISOFile( path=ref.path, size=ref.total_size, date=ref.date, is_dir=ref.is_dir, extents=ref.extents, inline=ref.inline_data, key=(FileSystemType.UDF, ref), ) elif self._iso is not None: for ref in self._iso.entries(): yield ISOFile( path=ref.path, size=ref.total_size, date=ref.date, is_dir=ref.is_dir, extents=ref.extents, key=(FileSystemType.ISO, ref), ) def extract(self, entry: ISOFile) -> bytes | bytearray: from refinery.lib.iso.iso9660 import ISORef from refinery.lib.iso.udf import UDFRef backend_id, ref = entry.key if isinstance(ref, ISORef) and (iso := self._iso): if backend_id != FileSystemType.ISO: raise ValueError(F'File System Inconsistency; expected ISO, got {backend_id.name}.') return iso.extract(ref) if isinstance(ref, UDFRef) and (udf := self._udf): if backend_id != FileSystemType.UDF: raise ValueError(F'File System Inconsistency; expected UDF, got {backend_id.name}.') return udf.extract(ref) return b''Instance variables
var filesystem_type-
Expand source code Browse git
@property def filesystem_type(self) -> str: return self._type.value
Methods
def select_filesystem(self, fs)-
Select the given file system and return whether the image contains it. Otherwise, the previous selection remains, except that requesting Rock Ridge while ISO 9660 is selected yields the plain ISO 9660 names when the primary volume lacks these extensions.
Expand source code Browse git
def select_filesystem(self, fs: FileSystemType) -> bool: """ Select the given file system and return whether the image contains it. Otherwise, the previous selection remains, except that requesting Rock Ridge while ISO 9660 is selected yields the plain ISO 9660 names when the primary volume lacks these extensions. """ if fs is FileSystemType.UDF: if self._udf is None: from refinery.lib.iso.udf import UDFArchive udf = UDFArchive() try: udf.open(self._data) except Exception: return False if not udf.refs: return False self._udf = udf self._type = FileSystemType.UDF return True if fs not in (FileSystemType.JOLIET, FileSystemType.RR, FileSystemType.ISO): return False if (iso := self._iso) is None: from refinery.lib.iso.iso9660 import ISO9660Archive self._iso = iso = ISO9660Archive() iso.open(self._data) selected = iso.select_filesystem(fs) if selected or self._type is not FileSystemType.UDF: self._type = iso.filesystem_type return selected def entries(self)-
Expand source code Browse git
def entries(self): if self._type is FileSystemType.UDF and self._udf is not None: for ref in self._udf.entries(): yield ISOFile( path=ref.path, size=ref.total_size, date=ref.date, is_dir=ref.is_dir, extents=ref.extents, inline=ref.inline_data, key=(FileSystemType.UDF, ref), ) elif self._iso is not None: for ref in self._iso.entries(): yield ISOFile( path=ref.path, size=ref.total_size, date=ref.date, is_dir=ref.is_dir, extents=ref.extents, key=(FileSystemType.ISO, ref), ) def extract(self, entry)-
Expand source code Browse git
def extract(self, entry: ISOFile) -> bytes | bytearray: from refinery.lib.iso.iso9660 import ISORef from refinery.lib.iso.udf import UDFRef backend_id, ref = entry.key if isinstance(ref, ISORef) and (iso := self._iso): if backend_id != FileSystemType.ISO: raise ValueError(F'File System Inconsistency; expected ISO, got {backend_id.name}.') return iso.extract(ref) if isinstance(ref, UDFRef) and (udf := self._udf): if backend_id != FileSystemType.UDF: raise ValueError(F'File System Inconsistency; expected UDF, got {backend_id.name}.') return udf.extract(ref) return b''