-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchains.py
More file actions
executable file
·73 lines (60 loc) · 2.65 KB
/
Copy pathchains.py
File metadata and controls
executable file
·73 lines (60 loc) · 2.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
"""chains.py — Episode I's two bigfile chains.
A TOC's sector numbers address a virtual byte space formed by concatenating
the TOC file itself with its sibling bigfiles, in order:
chain 0: XENOSAGA.00 + XENOSAGA.01 + XENOSAGA.02 (system / field data)
chain 1: XENOSAGA.10 + XENOSAGA.11 + XENOSAGA.12 + XENOSAGA.13
(streaming data: voice .vds/.vdm, scene .fpk/.arc/.evt, movie .pss)
Verified: the maximum extent of every entry in each TOC lands exactly at (or
within 384 bytes of) the end of its chain, and reads at computed offsets
produce the right magics (e.g. ``movie\\mpeg2\\2034.pss`` → MPEG-2 pack header).
"""
from __future__ import annotations
from typing import Iterator, List, Tuple
from iso9660 import IsoImage
CHAINS: dict[int, List[str]] = {
0: ["XENOSAGA.00", "XENOSAGA.01", "XENOSAGA.02"],
1: ["XENOSAGA.10", "XENOSAGA.11", "XENOSAGA.12", "XENOSAGA.13"],
}
CHUNK = 8 * 1024 * 1024
class ChainReader:
def __init__(self, iso: IsoImage, chain: int):
self.iso = iso
self.names = CHAINS[chain]
for n in self.names:
if n not in iso.files:
raise FileNotFoundError(f"{n} missing from ISO filesystem")
self.sizes = [iso.files[n].size for n in self.names]
self.total = sum(self.sizes)
def toc_bytes(self) -> bytes:
return self.iso.read_file(self.names[0])
def read_iter(self, offset: int, length: int) -> Iterator[bytes]:
"""Yield the byte range [offset, offset+length) of the chain in chunks,
transparently spanning bigfile boundaries."""
if offset + length > self.total:
raise ValueError(
f"read past chain end: {offset}+{length} > {self.total}"
)
remaining = length
for name, size in zip(self.names, self.sizes):
if offset >= size:
offset -= size
continue
take = min(remaining, size - offset)
pos = 0
while pos < take:
n = min(CHUNK, take - pos)
yield self.iso.read_file(name, offset + pos, n)
pos += n
remaining -= take
offset = 0
if remaining == 0:
return
def read(self, offset: int, length: int) -> bytes:
return b"".join(self.read_iter(offset, length))
def locate(self, offset: int) -> Tuple[str, int]:
"""Map a chain byte offset to (bigfile name, offset within it)."""
for name, size in zip(self.names, self.sizes):
if offset < size:
return name, offset
offset -= size
raise ValueError(f"offset {offset} beyond chain end")