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 descriptors BEA01, NSR02 or NSR03, and TEA01.

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 = 3

Access 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 = key

Instance 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''