Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
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.streams import EndianBinaryReader
|
|
from UnityPy.helpers.CompressionHelper import DECOMPRESSION_MAP
|
|
|
|
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
|