- Rate-limit failed logins (8 per 15 min per IP or username, 429 + Retry-After) - Add ruff/mypy config and GitHub Actions workflow (ruff, pytest, tsc, tests, build) - Apply ruff autofixes (import order, pyupgrade) - Extract rpy_helpers.py, engine-util.ts, TranslationParts.tsx and translation-types.ts from oversized modules - Add dependency-free React UI tests - Refresh README and ARCHITECTURE (Naninovel/Tomie, status and plans) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
66 lines
3.2 KiB
Python
66 lines
3.2 KiB
Python
"""Stream UnityFS nodes to a private workspace; do not load media bundles into RAM."""
|
|
from pathlib import Path
|
|
|
|
|
|
def unpack(source: Path, target: Path) -> list[Path]:
|
|
from UnityPy.helpers.CompressionHelper import DECOMPRESSION_MAP
|
|
from UnityPy.streams import EndianBinaryReader
|
|
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
with source.open("rb") as stream:
|
|
reader = EndianBinaryReader(stream)
|
|
if reader.read_string_to_null() != "UnityFS":
|
|
raise ValueError(f"Неподдерживаемый Unity-контейнер: {source.name}")
|
|
version = reader.read_u_int()
|
|
reader.read_string_to_null()
|
|
reader.read_string_to_null()
|
|
size = reader.read_long()
|
|
packed, raw, flags = reader.read_u_int(), reader.read_u_int(), reader.read_u_int()
|
|
if version not in {6, 7, 8} or flags & ~0x3ff:
|
|
raise ValueError(f"Неподдерживаемый UnityFS: версия {version}, flags={flags}")
|
|
if version >= 7:
|
|
reader.align_stream(16)
|
|
start = reader.Position
|
|
if flags & 0x80:
|
|
reader.Position = size - packed
|
|
info = EndianBinaryReader(DECOMPRESSION_MAP[flags & 63](reader.read_bytes(packed), raw))
|
|
if flags & 0x80:
|
|
reader.Position = start
|
|
info.read_bytes(16)
|
|
count = info.read_int()
|
|
if not 0 <= count <= 1_000_000:
|
|
raise ValueError("Некорректное количество блоков UnityFS")
|
|
blocks = [(info.read_u_int(), info.read_u_int(), info.read_u_short()) for _ in range(count)]
|
|
count = info.read_int()
|
|
if not 0 <= count <= 100_000:
|
|
raise ValueError("Некорректное количество файлов UnityFS")
|
|
nodes = [(info.read_long(), info.read_long(), info.read_u_int(), info.read_string_to_null())
|
|
for _ in range(count)]
|
|
if flags & 0x200:
|
|
reader.align_stream(16)
|
|
handles, paths = [], []
|
|
total = sum(b[0] for b in blocks)
|
|
try:
|
|
for offset, length, _, name in nodes:
|
|
path = (target / name).resolve()
|
|
if (not path.is_relative_to(target.resolve()) or path in paths or
|
|
offset < 0 or length < 0 or offset + length > total):
|
|
raise ValueError(f"Некорректный файл UnityFS: {name}")
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
handles.append((offset, length, path.open("wb")))
|
|
paths.append(path)
|
|
position = 0
|
|
for raw_size, packed_size, block_flags in blocks:
|
|
chunk = DECOMPRESSION_MAP[block_flags & 63](reader.read_bytes(packed_size), raw_size)
|
|
if len(chunk) != raw_size:
|
|
raise ValueError("Повреждённый блок UnityFS")
|
|
for offset, length, handle in handles:
|
|
left, right = max(position, offset), min(position + raw_size, offset + length)
|
|
if left < right:
|
|
handle.write(chunk[left - position:right - position])
|
|
position += raw_size
|
|
finally:
|
|
for _, _, handle in handles:
|
|
handle.close()
|
|
return paths
|