Module refinery.lib.iso.iso9660
ISO 9660 filesystem parser ported from 7zip's Archive/Iso/ implementation.
Expand source code Browse git
"""
ISO 9660 filesystem parser ported from 7zip's Archive/Iso/ implementation.
"""
from __future__ import annotations
import itertools
import struct
from datetime import datetime, timedelta, timezone
from typing import Callable, Iterator
from refinery.lib.id import buffer_offset
from refinery.lib.iso import FileSystemType
BLOCK_SIZE = 2048
START_POS = 0x8000
VD_TYPE_BOOT = 0
VD_TYPE_PRIMARY = 1
VD_TYPE_SUPPLEMENTARY = 2
VD_TYPE_PARTITION = 3
VD_TYPE_TERMINATOR = 255
FILE_FLAG_DIR = 0x02
FILE_FLAG_NON_FINAL_EXTENT = 0x80
SUSP_TAG_SP = b'SP'
SUSP_TAG_CE = b'CE'
SUSP_TAG_NM = b'NM'
SUSP_TAG_PX = b'PX'
SUSP_TAG_SL = b'SL'
SUSP_TAG_RR = b'RR'
MAX_DIR_DEPTH = 256
MAX_DIR_RECORDS = 0x100000
def _read_both_endian_u32(data: bytes | memoryview, offset: int) -> int:
return struct.unpack_from('<I', data, offset)[0]
def _read_both_endian_u16(data: bytes | memoryview, offset: int) -> int:
return struct.unpack_from('<H', data, offset)[0]
def _parse_recording_datetime(data: bytes | memoryview) -> datetime | None:
if len(data) < 7:
return None
year = data[0] + 1900
month = data[1]
day = data[2]
hour = data[3]
minute = data[4]
second = data[5]
gmt_offset = struct.unpack_from('b', data, 6)[0]
if month < 1 or month > 12:
return None
if day < 1 or day > 31:
return None
try:
tz = timezone(timedelta(minutes=15 * gmt_offset))
return datetime(year, month, day, hour, minute, second, tzinfo=tz)
except (ValueError, OverflowError):
return None
class DirRecord:
__slots__ = (
'extent_location',
'data_length',
'datetime',
'file_flags',
'file_id',
'system_use',
)
def __init__(self):
self.extent_location: int = 0
self.data_length: int = 0
self.datetime: datetime | None = None
self.file_flags: int = 0
self.file_id: bytes | memoryview = b''
self.system_use: bytes | memoryview = b''
@property
def is_dir(self) -> bool:
return bool(self.file_flags & FILE_FLAG_DIR)
@property
def is_non_final_extent(self) -> bool:
return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT)
@property
def is_system_item(self) -> bool:
return self.file_id == b'\x00' or self.file_id == b'\x01'
class VolumeDescriptor:
__slots__ = (
'vd_type',
'system_id',
'volume_id',
'volume_space_size',
'volume_set_size',
'volume_sequence_number',
'logical_block_size',
'path_table_size',
'root_dir_record',
'escape_sequence',
'is_supplementary',
)
def __init__(self):
self.vd_type: int = 0
self.system_id: bytes | memoryview = b''
self.volume_id: bytes | memoryview = b''
self.volume_space_size: int = 0
self.volume_set_size: int = 0
self.volume_sequence_number: int = 0
self.logical_block_size: int = 0
self.path_table_size: int = 0
self.root_dir_record: DirRecord | None = None
self.escape_sequence: bytes | memoryview = b''
self.is_supplementary: bool = False
@property
def is_joliet(self) -> bool:
es = self.escape_sequence[:3]
return es == b'%/@' or es == b'%/C' or es == b'%/E'
class ISORef:
__slots__ = ('path', 'date', 'extents', 'is_dir', '_inline')
def __init__(self, path: str, date: datetime | None, is_dir: bool = False):
self.path = path
self.date = date
self.extents: list[tuple[int, int]] = []
self.is_dir = is_dir
self._inline: bytes | None = None
@property
def total_size(self) -> int:
if self._inline is not None:
return len(self._inline)
return sum(s for _, s in self.extents)
def _find_susp_entry(system_use: bytes | memoryview, tag: bytes, offset: int = 0) -> bytes | memoryview | None:
pos = offset
while pos + 4 <= len(system_use):
entry_tag = system_use[pos:pos + 2]
entry_len = system_use[pos + 2]
if entry_len < 4:
break
if pos + entry_len > len(system_use):
break
if entry_tag == tag:
return system_use[pos:pos + entry_len]
pos += entry_len
return None
def _get_susp_ce(system_use: bytes | memoryview, susp_offset: int = 0) -> tuple[int, int, int] | None:
entry = _find_susp_entry(system_use, SUSP_TAG_CE, susp_offset)
if entry is None or len(entry) < 28:
return None
block = _read_both_endian_u32(entry, 4)
offset = _read_both_endian_u32(entry, 12)
length = _read_both_endian_u32(entry, 20)
return (block, offset, length)
def _check_susp(root_record: DirRecord) -> int:
su = root_record.system_use
for offset in (0, 14):
sp = _find_susp_entry(su, SUSP_TAG_SP, offset)
if sp and len(sp) >= 7 and sp[4] == 0xBE and sp[5] == 0xEF:
skip_len = sp[6]
return offset + skip_len
return -1
def _get_name_rr(
system_use: bytes | memoryview,
susp_offset: int,
data: memoryview
) -> str | None:
parts: list[bytes | memoryview] = []
su = system_use
su_off = susp_offset
visited_ce: set[int] = set()
while True:
nm = _find_susp_entry(su, SUSP_TAG_NM, su_off)
if nm is None or len(nm) < 5:
break
flags = nm[4]
name_part = nm[5:]
parts.append(name_part)
if not (flags & 0x01):
break
su_off_next = buffer_offset(su, nm, su_off)
if su_off_next < 0:
su_off_next = len(su)
else:
su_off_next = len(nm) + su_off_next
ce = _get_susp_ce(su, su_off)
if ce:
block, off, length = ce
ce_pos = block * BLOCK_SIZE + off
if ce_pos not in visited_ce and ce_pos + length <= len(data):
visited_ce.add(ce_pos)
su = data[ce_pos:ce_pos + length]
su_off = 0
continue
su_off = su_off_next
if not parts:
return None
return b''.join(parts).decode('utf-8', errors='replace')
def _get_symlink_rr(
system_use: bytes | memoryview,
susp_offset: int,
data: memoryview
) -> str | None:
entry = _find_susp_entry(system_use, SUSP_TAG_SL, susp_offset)
if entry is None or len(entry) < 5:
return None
return None
def _has_rr(system_use: bytes | memoryview, susp_offset: int) -> bool:
entry = _find_susp_entry(system_use, SUSP_TAG_RR, susp_offset)
if entry is not None:
return True
entry = _find_susp_entry(system_use, SUSP_TAG_PX, susp_offset)
return entry is not None
def _fat_datetime(entry: bytes | memoryview) -> datetime | None:
time_val = struct.unpack_from('<H', entry, 22)[0]
date_val = struct.unpack_from('<H', entry, 24)[0]
if date_val == 0:
return None
try:
return datetime(
((date_val >> 9) & 0x7F) + 1980,
(date_val >> 5) & 0xF,
date_val & 0x1F,
(time_val >> 11) & 0x1F,
(time_val >> 5) & 0x3F,
(time_val & 0x1F) * 2,
)
except (ValueError, OverflowError):
return None
def _walk_fat_directory(
dir_data: bytes | memoryview,
prefix: str,
depth: int,
extract_chain: Callable[[int, int], bytes],
cluster_bytes: int,
walked: set[int],
) -> Iterator[tuple[str, datetime | None, bytes]]:
"""
Generates a *(path, date, content)* triple for every file in the given FAT directory and in
its subdirectories, each subdirectory in place of its entry. The cluster chain of a file or
directory is read by `extract_chain` from its first cluster and its size. A subdirectory whose
first cluster is in `walked` was listed already and is skipped; the others are added to it.
"""
if depth > 16:
return
for i in range(0, len(dir_data) - 31, 32):
entry = dir_data[i:i + 32]
if entry[0] == 0:
break
if entry[0] == 0xE5:
continue
attr = entry[11]
if attr == 0x0F or attr & 0x08:
continue
raw_name = bytes(entry[0:8]).rstrip(b' ')
raw_ext = bytes(entry[8:11]).rstrip(b' ')
try:
fname = raw_name.decode('ascii')
if raw_ext:
fname += '.' + raw_ext.decode('ascii')
except (UnicodeDecodeError, ValueError):
continue
fname = fname.lower()
cluster = struct.unpack_from('<H', entry, 26)[0]
size = struct.unpack_from('<I', entry, 28)[0]
dt = _fat_datetime(entry)
full_path = F'{prefix}/{fname}' if prefix else fname
if attr & 0x10:
if fname in ('.', '..') or cluster in walked:
continue
walked.add(cluster)
sub = extract_chain(cluster, cluster_bytes * 256)
yield from _walk_fat_directory(
sub, full_path, depth + 1, extract_chain, cluster_bytes, walked
)
else:
yield full_path, dt, extract_chain(cluster, size)
def _parse_fat_image(
data: memoryview | bytes,
) -> list[tuple[str, datetime | None, bytes]] | None:
"""Parse a FAT12/FAT16 floppy or hard-disk image embedded in an El Torito
boot entry and return a list of *(path, date, content)* triples. Returns
``None`` when the image does not look like a valid FAT volume.
"""
if len(data) < 512:
return None
bps = struct.unpack_from('<H', data, 11)[0]
if bps not in (512, 1024, 2048, 4096):
return None
spc = data[13]
if spc == 0 or spc & (spc - 1):
return None
reserved = struct.unpack_from('<H', data, 14)[0]
num_fats = data[16]
root_entries = struct.unpack_from('<H', data, 17)[0]
total_sectors = struct.unpack_from('<H', data, 19)[0]
if total_sectors == 0:
total_sectors = struct.unpack_from('<I', data, 32)[0]
spf = struct.unpack_from('<H', data, 22)[0]
if num_fats == 0 or spf == 0 or root_entries == 0:
return None
fat_offset = reserved * bps
root_dir_offset = fat_offset + num_fats * spf * bps
root_dir_size = root_entries * 32
data_offset = root_dir_offset + root_dir_size
if data_offset > len(data):
return None
root_dir_sectors = (root_entries * 32 + bps - 1) // bps
total_data_sectors = total_sectors - (reserved + num_fats * spf + root_dir_sectors)
total_clusters = total_data_sectors // spc
is_fat16 = total_clusters >= 4085
cluster_bytes = spc * bps
fat = data[fat_offset:fat_offset + spf * bps]
def next_cluster(c: int) -> int:
if is_fat16:
off = c * 2
if off + 2 > len(fat):
return 0xFFFF
return struct.unpack_from('<H', fat, off)[0]
off = c * 3 // 2
if off + 2 > len(fat):
return 0xFFF
val = struct.unpack_from('<H', fat, off)[0]
return val & 0xFFF if c % 2 == 0 else val >> 4
end_mark = 0xFFF8 if is_fat16 else 0xFF8
def extract_chain(start: int, size: int) -> bytes:
result = bytearray()
remaining = size
chain: set[int] = set()
c = start
while c >= 2 and c < end_mark and remaining > 0 and c not in chain:
chain.add(c)
cluster_off = data_offset + (c - 2) * cluster_bytes
chunk = min(cluster_bytes, remaining)
if cluster_off + chunk > len(data):
break
result.extend(data[cluster_off:cluster_off + chunk])
remaining -= chunk
c = next_cluster(c)
return bytes(result[:size])
root = data[root_dir_offset:root_dir_offset + root_dir_size]
files = list(_walk_fat_directory(root, '', 0, extract_chain, cluster_bytes, set()))
return files or None
class ISO9660Archive:
def __init__(self):
self.refs: list[ISORef] = []
self._data: memoryview = memoryview(b'')
self._block_size: int = BLOCK_SIZE
self.filesystem_type: FileSystemType = FileSystemType.ISO
self._has_rr: bool = False
self._has_joliet: bool = False
self._primary_vd: VolumeDescriptor | None = None
self._joliet_vd: VolumeDescriptor | None = None
self._boot_record_found: bool = False
self._boot_catalog_location: int = 0
def open(self, data: bytes | bytearray | memoryview) -> None:
self._data = memoryview(data)
volume_descriptors: list[VolumeDescriptor] = []
pos = START_POS
while pos + BLOCK_SIZE <= len(self._data):
block = self._data[pos:pos + BLOCK_SIZE]
if len(block) < 7:
break
vd_type = block[0]
magic = block[1:6]
if magic != b'CD001':
break
if vd_type == VD_TYPE_TERMINATOR:
pos += BLOCK_SIZE
break
elif vd_type == VD_TYPE_BOOT:
self._parse_boot_record(block)
pos += BLOCK_SIZE
continue
elif vd_type == VD_TYPE_PRIMARY or vd_type == VD_TYPE_SUPPLEMENTARY:
vd = self._parse_volume_descriptor(block, vd_type)
if vd is not None:
volume_descriptors.append(vd)
pos += BLOCK_SIZE
primary: VolumeDescriptor | None = None
joliet: VolumeDescriptor | None = None
for vd in volume_descriptors:
if vd.is_supplementary and vd.is_joliet:
joliet = vd
elif not vd.is_supplementary:
primary = vd
self._primary_vd = primary
self._joliet_vd = joliet
self._has_joliet = joliet is not None
if joliet:
self._read_directory_tree(joliet, is_joliet=True)
elif primary:
self._read_directory_tree(primary, is_joliet=False)
if self._boot_record_found:
self._extract_boot_images()
def _parse_boot_record(self, block: bytes | memoryview) -> None:
boot_system_id = bytes(block[7:39])
if boot_system_id.rstrip(b'\x00').rstrip(b'\x20') == b'EL TORITO SPECIFICATION':
self._boot_record_found = True
self._boot_catalog_location = struct.unpack_from('<I', block, 71)[0]
def _parse_volume_descriptor(
self,
block: bytes | memoryview,
vd_type: int
) -> VolumeDescriptor | None:
vd = VolumeDescriptor()
vd.vd_type = vd_type
vd.is_supplementary = (vd_type == VD_TYPE_SUPPLEMENTARY)
vd.system_id = block[8:40]
vd.volume_id = block[40:72]
vd.volume_space_size = _read_both_endian_u32(block, 80)
vd.escape_sequence = block[88:120]
if not vd.is_supplementary:
vd.escape_sequence = b''
vd.volume_set_size = _read_both_endian_u16(block, 120)
vd.volume_sequence_number = _read_both_endian_u16(block, 124)
vd.logical_block_size = _read_both_endian_u16(block, 128)
vd.path_table_size = _read_both_endian_u32(block, 132)
root_record_data = block[156:190]
if len(root_record_data) >= 34:
vd.root_dir_record = self._parse_dir_record(root_record_data)
return vd
def _parse_dir_record(self, data: bytes | memoryview) -> DirRecord | None:
if len(data) < 34:
return None
rec = DirRecord()
record_len = data[0]
if record_len < 34:
record_len = 34
rec.extent_location = _read_both_endian_u32(data, 2)
rec.data_length = _read_both_endian_u32(data, 10)
rec.datetime = _parse_recording_datetime(data[18:25])
rec.file_flags = data[25]
file_id_len = data[32]
if 33 + file_id_len > len(data):
file_id_len = len(data) - 33
rec.file_id = data[33:33 + file_id_len]
su_start = 33 + file_id_len
if file_id_len % 2 == 0:
su_start += 1
if su_start < len(data):
rec.system_use = data[su_start:min(len(data), record_len)]
return rec
def _read_directory_tree(
self,
vd: VolumeDescriptor,
is_joliet: bool
) -> None:
root = vd.root_dir_record
if root is None:
return
susp_offset = -1
has_rr = False
if not is_joliet:
actual_root = self._read_root_dot_entry(root)
susp_root = actual_root or root
susp_offset = _check_susp(susp_root)
if susp_offset >= 0:
has_rr = _has_rr(susp_root.system_use, susp_offset)
self._has_rr = has_rr
if is_joliet:
self.filesystem_type = FileSystemType.JOLIET
elif has_rr:
self.filesystem_type = FileSystemType.RR
else:
self.filesystem_type = FileSystemType.ISO
self.refs.clear()
visited: set[int] = set()
self._walk_directory(
root.extent_location,
root.data_length,
'',
is_joliet,
has_rr,
susp_offset,
visited,
0,
)
def _walk_directory(
self,
extent_loc: int,
extent_size: int,
parent_path: str,
is_joliet: bool,
has_rr: bool,
susp_offset: int,
visited: set[int],
depth: int,
) -> None:
if depth > MAX_DIR_DEPTH:
return
if extent_loc in visited:
return
visited.add(extent_loc)
dir_pos = extent_loc * self._block_size
dir_end = dir_pos + extent_size
if dir_pos >= len(self._data) or dir_end > len(self._data):
dir_end = min(dir_end, len(self._data))
pos = dir_pos
record_count = 0
pending_ref: ISORef | None = None
while pos < dir_end and record_count < MAX_DIR_RECORDS:
if pos >= len(self._data):
break
record_len = self._data[pos]
if record_len == 0:
next_block = ((pos - dir_pos) // self._block_size + 1) * self._block_size + dir_pos
if next_block >= dir_end:
break
pos = next_block
continue
if record_len < 34:
pos += 1
continue
end = pos + record_len
if end > len(self._data):
break
record_data = self._data[pos:end]
rec = self._parse_dir_record(record_data)
pos = end
record_count += 1
if rec is None:
continue
if rec.is_system_item:
continue
name = self._decode_name(rec, is_joliet, has_rr, susp_offset)
if not name:
continue
full_path = F'{parent_path}/{name}' if parent_path else name
if rec.is_non_final_extent and not rec.is_dir:
if pending_ref is None or pending_ref.path != full_path:
pending_ref = ISORef(full_path, rec.datetime)
self.refs.append(pending_ref)
pending_ref.extents.append((rec.extent_location, rec.data_length))
continue
elif pending_ref is not None and pending_ref.path == full_path:
pending_ref.extents.append((rec.extent_location, rec.data_length))
pending_ref = None
continue
else:
pending_ref = None
if rec.is_dir:
ref = ISORef(full_path, rec.datetime, is_dir=True)
self.refs.append(ref)
self._walk_directory(
rec.extent_location,
rec.data_length,
full_path,
is_joliet,
has_rr,
susp_offset,
visited,
depth + 1,
)
else:
ref = ISORef(full_path, rec.datetime)
ref.extents.append((rec.extent_location, rec.data_length))
self.refs.append(ref)
def _decode_name(
self,
rec: DirRecord,
is_joliet: bool,
has_rr: bool,
susp_offset: int,
) -> str:
if has_rr and susp_offset >= 0:
rr_name = _get_name_rr(rec.system_use, susp_offset, self._data)
if rr_name:
return rr_name
if is_joliet:
try:
name = bytes(rec.file_id).decode('utf-16-be')
except (UnicodeDecodeError, ValueError):
name = bytes(rec.file_id).decode('latin-1')
else:
name = bytes(rec.file_id).decode('latin-1')
name = self._strip_revision(name)
if name.endswith('.'):
name = name[:-1]
return name
@staticmethod
def _strip_revision(name: str) -> str:
base, sep, revision = name.partition(';')
if sep and revision.isdigit():
return base
return name
def select_filesystem(self, fs: FileSystemType) -> bool:
"""
Read the directory tree of the given file system and return whether the image contains it.
The directory tree remains unchanged when the image has no volume descriptor for it, and
Rock Ridge falls back to plain ISO 9660 names when the primary volume lacks its extensions.
"""
vd = self._joliet_vd if fs is FileSystemType.JOLIET else self._primary_vd
if vd is None:
return False
if fs is self.filesystem_type:
return True
if fs is FileSystemType.JOLIET:
self._read_directory_tree(vd, is_joliet=True)
elif fs is FileSystemType.RR:
self._read_directory_tree(vd, is_joliet=False)
elif fs is FileSystemType.ISO:
self._has_rr = False
self._read_directory_tree_plain(vd)
return fs is self.filesystem_type
def _read_directory_tree_plain(self, vd: VolumeDescriptor) -> None:
root = vd.root_dir_record
if root is None:
return
self.filesystem_type = FileSystemType.ISO
self.refs.clear()
visited: set[int] = set()
self._walk_directory(
root.extent_location,
root.data_length,
'',
False,
False,
-1,
visited,
0,
)
def _read_root_dot_entry(self, root: DirRecord) -> DirRecord | None:
"""Read the actual '.' entry from the root directory data on disk.
The root directory record stored in the Volume Descriptor is a fixed
34-byte copy that lacks the system use area. The real first entry in
the root directory sector may be much larger and contain SUSP/RR data.
"""
dir_pos = root.extent_location * self._block_size
if dir_pos >= len(self._data):
return None
rec_len = self._data[dir_pos]
if rec_len < 34:
return None
end = dir_pos + rec_len
if end > len(self._data):
return None
return self._parse_dir_record(self._data[dir_pos:end])
def _extract_boot_images(self) -> None:
from refinery.lib.iso.eltorito import BootCatalog
catalog_pos = self._boot_catalog_location * self._block_size
if catalog_pos + 2048 > len(self._data):
return
catalog_data = bytes(self._data[catalog_pos:catalog_pos + 2048])
catalog = BootCatalog.parse(catalog_data)
if catalog is None:
return
existing_extents: set[int] = set()
for ref in self.refs:
for block, _ in ref.extents:
existing_extents.add(block)
for entry in catalog.entries:
if not entry.is_bootable:
continue
if entry.load_rba in existing_extents:
continue
img_pos = entry.load_rba * self._block_size
img_size = entry.image_size
if img_pos >= len(self._data):
continue
if img_pos + img_size > len(self._data):
img_size = len(self._data) - img_pos
if img_size <= 0:
continue
fat_files = _parse_fat_image(self._data[img_pos:img_pos + img_size])
if fat_files:
for path, date, file_data in fat_files:
ref = ISORef(path, date)
ref._inline = file_data
self.refs.append(ref)
else:
ref = ISORef(entry.name, None)
ref.extents.append((entry.load_rba, img_size))
self.refs.append(ref)
def entries(self):
for ref in self.refs:
if not ref.is_dir:
yield ref
def extract(self, ref: ISORef) -> bytearray:
if ref._inline is not None:
return bytearray(ref._inline)
if not ref.extents:
return bytearray()
result = bytearray()
for block, size in ref.extents:
start = block * self._block_size
end = start + size
if end > len(self._data):
end = len(self._data)
if start >= len(self._data):
result.extend(itertools.repeat(0, size))
else:
result.extend(self._data[start:end])
return result
Classes
class DirRecord-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01'Instance variables
var is_dir-
Expand source code Browse git
@property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) var is_non_final_extent-
Expand source code Browse git
@property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) var is_system_item-
Expand source code Browse git
@property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var data_length-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var datetime-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var extent_location-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var file_flags-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var file_id-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01' var system_use-
Expand source code Browse git
class DirRecord: __slots__ = ( 'extent_location', 'data_length', 'datetime', 'file_flags', 'file_id', 'system_use', ) def __init__(self): self.extent_location: int = 0 self.data_length: int = 0 self.datetime: datetime | None = None self.file_flags: int = 0 self.file_id: bytes | memoryview = b'' self.system_use: bytes | memoryview = b'' @property def is_dir(self) -> bool: return bool(self.file_flags & FILE_FLAG_DIR) @property def is_non_final_extent(self) -> bool: return bool(self.file_flags & FILE_FLAG_NON_FINAL_EXTENT) @property def is_system_item(self) -> bool: return self.file_id == b'\x00' or self.file_id == b'\x01'
class VolumeDescriptor-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E'Instance variables
var is_joliet-
Expand source code Browse git
@property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var escape_sequence-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var is_supplementary-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var logical_block_size-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var path_table_size-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var root_dir_record-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var system_id-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var vd_type-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var volume_id-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var volume_sequence_number-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var volume_set_size-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E' var volume_space_size-
Expand source code Browse git
class VolumeDescriptor: __slots__ = ( 'vd_type', 'system_id', 'volume_id', 'volume_space_size', 'volume_set_size', 'volume_sequence_number', 'logical_block_size', 'path_table_size', 'root_dir_record', 'escape_sequence', 'is_supplementary', ) def __init__(self): self.vd_type: int = 0 self.system_id: bytes | memoryview = b'' self.volume_id: bytes | memoryview = b'' self.volume_space_size: int = 0 self.volume_set_size: int = 0 self.volume_sequence_number: int = 0 self.logical_block_size: int = 0 self.path_table_size: int = 0 self.root_dir_record: DirRecord | None = None self.escape_sequence: bytes | memoryview = b'' self.is_supplementary: bool = False @property def is_joliet(self) -> bool: es = self.escape_sequence[:3] return es == b'%/@' or es == b'%/C' or es == b'%/E'
class ISORef (path, date, is_dir=False)-
Expand source code Browse git
class ISORef: __slots__ = ('path', 'date', 'extents', 'is_dir', '_inline') def __init__(self, path: str, date: datetime | None, is_dir: bool = False): self.path = path self.date = date self.extents: list[tuple[int, int]] = [] self.is_dir = is_dir self._inline: bytes | None = None @property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents)Instance variables
var total_size-
Expand source code Browse git
@property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents) var date-
Expand source code Browse git
class ISORef: __slots__ = ('path', 'date', 'extents', 'is_dir', '_inline') def __init__(self, path: str, date: datetime | None, is_dir: bool = False): self.path = path self.date = date self.extents: list[tuple[int, int]] = [] self.is_dir = is_dir self._inline: bytes | None = None @property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents) var extents-
Expand source code Browse git
class ISORef: __slots__ = ('path', 'date', 'extents', 'is_dir', '_inline') def __init__(self, path: str, date: datetime | None, is_dir: bool = False): self.path = path self.date = date self.extents: list[tuple[int, int]] = [] self.is_dir = is_dir self._inline: bytes | None = None @property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents) var is_dir-
Expand source code Browse git
class ISORef: __slots__ = ('path', 'date', 'extents', 'is_dir', '_inline') def __init__(self, path: str, date: datetime | None, is_dir: bool = False): self.path = path self.date = date self.extents: list[tuple[int, int]] = [] self.is_dir = is_dir self._inline: bytes | None = None @property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents) var path-
Expand source code Browse git
class ISORef: __slots__ = ('path', 'date', 'extents', 'is_dir', '_inline') def __init__(self, path: str, date: datetime | None, is_dir: bool = False): self.path = path self.date = date self.extents: list[tuple[int, int]] = [] self.is_dir = is_dir self._inline: bytes | None = None @property def total_size(self) -> int: if self._inline is not None: return len(self._inline) return sum(s for _, s in self.extents)
class ISO9660Archive-
Expand source code Browse git
class ISO9660Archive: def __init__(self): self.refs: list[ISORef] = [] self._data: memoryview = memoryview(b'') self._block_size: int = BLOCK_SIZE self.filesystem_type: FileSystemType = FileSystemType.ISO self._has_rr: bool = False self._has_joliet: bool = False self._primary_vd: VolumeDescriptor | None = None self._joliet_vd: VolumeDescriptor | None = None self._boot_record_found: bool = False self._boot_catalog_location: int = 0 def open(self, data: bytes | bytearray | memoryview) -> None: self._data = memoryview(data) volume_descriptors: list[VolumeDescriptor] = [] pos = START_POS while pos + BLOCK_SIZE <= len(self._data): block = self._data[pos:pos + BLOCK_SIZE] if len(block) < 7: break vd_type = block[0] magic = block[1:6] if magic != b'CD001': break if vd_type == VD_TYPE_TERMINATOR: pos += BLOCK_SIZE break elif vd_type == VD_TYPE_BOOT: self._parse_boot_record(block) pos += BLOCK_SIZE continue elif vd_type == VD_TYPE_PRIMARY or vd_type == VD_TYPE_SUPPLEMENTARY: vd = self._parse_volume_descriptor(block, vd_type) if vd is not None: volume_descriptors.append(vd) pos += BLOCK_SIZE primary: VolumeDescriptor | None = None joliet: VolumeDescriptor | None = None for vd in volume_descriptors: if vd.is_supplementary and vd.is_joliet: joliet = vd elif not vd.is_supplementary: primary = vd self._primary_vd = primary self._joliet_vd = joliet self._has_joliet = joliet is not None if joliet: self._read_directory_tree(joliet, is_joliet=True) elif primary: self._read_directory_tree(primary, is_joliet=False) if self._boot_record_found: self._extract_boot_images() def _parse_boot_record(self, block: bytes | memoryview) -> None: boot_system_id = bytes(block[7:39]) if boot_system_id.rstrip(b'\x00').rstrip(b'\x20') == b'EL TORITO SPECIFICATION': self._boot_record_found = True self._boot_catalog_location = struct.unpack_from('<I', block, 71)[0] def _parse_volume_descriptor( self, block: bytes | memoryview, vd_type: int ) -> VolumeDescriptor | None: vd = VolumeDescriptor() vd.vd_type = vd_type vd.is_supplementary = (vd_type == VD_TYPE_SUPPLEMENTARY) vd.system_id = block[8:40] vd.volume_id = block[40:72] vd.volume_space_size = _read_both_endian_u32(block, 80) vd.escape_sequence = block[88:120] if not vd.is_supplementary: vd.escape_sequence = b'' vd.volume_set_size = _read_both_endian_u16(block, 120) vd.volume_sequence_number = _read_both_endian_u16(block, 124) vd.logical_block_size = _read_both_endian_u16(block, 128) vd.path_table_size = _read_both_endian_u32(block, 132) root_record_data = block[156:190] if len(root_record_data) >= 34: vd.root_dir_record = self._parse_dir_record(root_record_data) return vd def _parse_dir_record(self, data: bytes | memoryview) -> DirRecord | None: if len(data) < 34: return None rec = DirRecord() record_len = data[0] if record_len < 34: record_len = 34 rec.extent_location = _read_both_endian_u32(data, 2) rec.data_length = _read_both_endian_u32(data, 10) rec.datetime = _parse_recording_datetime(data[18:25]) rec.file_flags = data[25] file_id_len = data[32] if 33 + file_id_len > len(data): file_id_len = len(data) - 33 rec.file_id = data[33:33 + file_id_len] su_start = 33 + file_id_len if file_id_len % 2 == 0: su_start += 1 if su_start < len(data): rec.system_use = data[su_start:min(len(data), record_len)] return rec def _read_directory_tree( self, vd: VolumeDescriptor, is_joliet: bool ) -> None: root = vd.root_dir_record if root is None: return susp_offset = -1 has_rr = False if not is_joliet: actual_root = self._read_root_dot_entry(root) susp_root = actual_root or root susp_offset = _check_susp(susp_root) if susp_offset >= 0: has_rr = _has_rr(susp_root.system_use, susp_offset) self._has_rr = has_rr if is_joliet: self.filesystem_type = FileSystemType.JOLIET elif has_rr: self.filesystem_type = FileSystemType.RR else: self.filesystem_type = FileSystemType.ISO self.refs.clear() visited: set[int] = set() self._walk_directory( root.extent_location, root.data_length, '', is_joliet, has_rr, susp_offset, visited, 0, ) def _walk_directory( self, extent_loc: int, extent_size: int, parent_path: str, is_joliet: bool, has_rr: bool, susp_offset: int, visited: set[int], depth: int, ) -> None: if depth > MAX_DIR_DEPTH: return if extent_loc in visited: return visited.add(extent_loc) dir_pos = extent_loc * self._block_size dir_end = dir_pos + extent_size if dir_pos >= len(self._data) or dir_end > len(self._data): dir_end = min(dir_end, len(self._data)) pos = dir_pos record_count = 0 pending_ref: ISORef | None = None while pos < dir_end and record_count < MAX_DIR_RECORDS: if pos >= len(self._data): break record_len = self._data[pos] if record_len == 0: next_block = ((pos - dir_pos) // self._block_size + 1) * self._block_size + dir_pos if next_block >= dir_end: break pos = next_block continue if record_len < 34: pos += 1 continue end = pos + record_len if end > len(self._data): break record_data = self._data[pos:end] rec = self._parse_dir_record(record_data) pos = end record_count += 1 if rec is None: continue if rec.is_system_item: continue name = self._decode_name(rec, is_joliet, has_rr, susp_offset) if not name: continue full_path = F'{parent_path}/{name}' if parent_path else name if rec.is_non_final_extent and not rec.is_dir: if pending_ref is None or pending_ref.path != full_path: pending_ref = ISORef(full_path, rec.datetime) self.refs.append(pending_ref) pending_ref.extents.append((rec.extent_location, rec.data_length)) continue elif pending_ref is not None and pending_ref.path == full_path: pending_ref.extents.append((rec.extent_location, rec.data_length)) pending_ref = None continue else: pending_ref = None if rec.is_dir: ref = ISORef(full_path, rec.datetime, is_dir=True) self.refs.append(ref) self._walk_directory( rec.extent_location, rec.data_length, full_path, is_joliet, has_rr, susp_offset, visited, depth + 1, ) else: ref = ISORef(full_path, rec.datetime) ref.extents.append((rec.extent_location, rec.data_length)) self.refs.append(ref) def _decode_name( self, rec: DirRecord, is_joliet: bool, has_rr: bool, susp_offset: int, ) -> str: if has_rr and susp_offset >= 0: rr_name = _get_name_rr(rec.system_use, susp_offset, self._data) if rr_name: return rr_name if is_joliet: try: name = bytes(rec.file_id).decode('utf-16-be') except (UnicodeDecodeError, ValueError): name = bytes(rec.file_id).decode('latin-1') else: name = bytes(rec.file_id).decode('latin-1') name = self._strip_revision(name) if name.endswith('.'): name = name[:-1] return name @staticmethod def _strip_revision(name: str) -> str: base, sep, revision = name.partition(';') if sep and revision.isdigit(): return base return name def select_filesystem(self, fs: FileSystemType) -> bool: """ Read the directory tree of the given file system and return whether the image contains it. The directory tree remains unchanged when the image has no volume descriptor for it, and Rock Ridge falls back to plain ISO 9660 names when the primary volume lacks its extensions. """ vd = self._joliet_vd if fs is FileSystemType.JOLIET else self._primary_vd if vd is None: return False if fs is self.filesystem_type: return True if fs is FileSystemType.JOLIET: self._read_directory_tree(vd, is_joliet=True) elif fs is FileSystemType.RR: self._read_directory_tree(vd, is_joliet=False) elif fs is FileSystemType.ISO: self._has_rr = False self._read_directory_tree_plain(vd) return fs is self.filesystem_type def _read_directory_tree_plain(self, vd: VolumeDescriptor) -> None: root = vd.root_dir_record if root is None: return self.filesystem_type = FileSystemType.ISO self.refs.clear() visited: set[int] = set() self._walk_directory( root.extent_location, root.data_length, '', False, False, -1, visited, 0, ) def _read_root_dot_entry(self, root: DirRecord) -> DirRecord | None: """Read the actual '.' entry from the root directory data on disk. The root directory record stored in the Volume Descriptor is a fixed 34-byte copy that lacks the system use area. The real first entry in the root directory sector may be much larger and contain SUSP/RR data. """ dir_pos = root.extent_location * self._block_size if dir_pos >= len(self._data): return None rec_len = self._data[dir_pos] if rec_len < 34: return None end = dir_pos + rec_len if end > len(self._data): return None return self._parse_dir_record(self._data[dir_pos:end]) def _extract_boot_images(self) -> None: from refinery.lib.iso.eltorito import BootCatalog catalog_pos = self._boot_catalog_location * self._block_size if catalog_pos + 2048 > len(self._data): return catalog_data = bytes(self._data[catalog_pos:catalog_pos + 2048]) catalog = BootCatalog.parse(catalog_data) if catalog is None: return existing_extents: set[int] = set() for ref in self.refs: for block, _ in ref.extents: existing_extents.add(block) for entry in catalog.entries: if not entry.is_bootable: continue if entry.load_rba in existing_extents: continue img_pos = entry.load_rba * self._block_size img_size = entry.image_size if img_pos >= len(self._data): continue if img_pos + img_size > len(self._data): img_size = len(self._data) - img_pos if img_size <= 0: continue fat_files = _parse_fat_image(self._data[img_pos:img_pos + img_size]) if fat_files: for path, date, file_data in fat_files: ref = ISORef(path, date) ref._inline = file_data self.refs.append(ref) else: ref = ISORef(entry.name, None) ref.extents.append((entry.load_rba, img_size)) self.refs.append(ref) def entries(self): for ref in self.refs: if not ref.is_dir: yield ref def extract(self, ref: ISORef) -> bytearray: if ref._inline is not None: return bytearray(ref._inline) if not ref.extents: return bytearray() result = bytearray() for block, size in ref.extents: start = block * self._block_size end = start + size if end > len(self._data): end = len(self._data) if start >= len(self._data): result.extend(itertools.repeat(0, size)) else: result.extend(self._data[start:end]) return resultMethods
def open(self, data)-
Expand source code Browse git
def open(self, data: bytes | bytearray | memoryview) -> None: self._data = memoryview(data) volume_descriptors: list[VolumeDescriptor] = [] pos = START_POS while pos + BLOCK_SIZE <= len(self._data): block = self._data[pos:pos + BLOCK_SIZE] if len(block) < 7: break vd_type = block[0] magic = block[1:6] if magic != b'CD001': break if vd_type == VD_TYPE_TERMINATOR: pos += BLOCK_SIZE break elif vd_type == VD_TYPE_BOOT: self._parse_boot_record(block) pos += BLOCK_SIZE continue elif vd_type == VD_TYPE_PRIMARY or vd_type == VD_TYPE_SUPPLEMENTARY: vd = self._parse_volume_descriptor(block, vd_type) if vd is not None: volume_descriptors.append(vd) pos += BLOCK_SIZE primary: VolumeDescriptor | None = None joliet: VolumeDescriptor | None = None for vd in volume_descriptors: if vd.is_supplementary and vd.is_joliet: joliet = vd elif not vd.is_supplementary: primary = vd self._primary_vd = primary self._joliet_vd = joliet self._has_joliet = joliet is not None if joliet: self._read_directory_tree(joliet, is_joliet=True) elif primary: self._read_directory_tree(primary, is_joliet=False) if self._boot_record_found: self._extract_boot_images() def select_filesystem(self, fs)-
Read the directory tree of the given file system and return whether the image contains it. The directory tree remains unchanged when the image has no volume descriptor for it, and Rock Ridge falls back to plain ISO 9660 names when the primary volume lacks its extensions.
Expand source code Browse git
def select_filesystem(self, fs: FileSystemType) -> bool: """ Read the directory tree of the given file system and return whether the image contains it. The directory tree remains unchanged when the image has no volume descriptor for it, and Rock Ridge falls back to plain ISO 9660 names when the primary volume lacks its extensions. """ vd = self._joliet_vd if fs is FileSystemType.JOLIET else self._primary_vd if vd is None: return False if fs is self.filesystem_type: return True if fs is FileSystemType.JOLIET: self._read_directory_tree(vd, is_joliet=True) elif fs is FileSystemType.RR: self._read_directory_tree(vd, is_joliet=False) elif fs is FileSystemType.ISO: self._has_rr = False self._read_directory_tree_plain(vd) return fs is self.filesystem_type def entries(self)-
Expand source code Browse git
def entries(self): for ref in self.refs: if not ref.is_dir: yield ref def extract(self, ref)-
Expand source code Browse git
def extract(self, ref: ISORef) -> bytearray: if ref._inline is not None: return bytearray(ref._inline) if not ref.extents: return bytearray() result = bytearray() for block, size in ref.extents: start = block * self._block_size end = start + size if end > len(self._data): end = len(self._data) if start >= len(self._data): result.extend(itertools.repeat(0, size)) else: result.extend(self._data[start:end]) return result