78 lines
2.4 KiB
Python
Executable File
78 lines
2.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Extract a single compressed RED4 archive segment by resource hash."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
|
|
FILE_ENTRY_SIZE = 56
|
|
FILE_SEGMENT_SIZE = 16
|
|
|
|
|
|
def parse_hash(value: str) -> int:
|
|
return int(value, 0)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("archive", type=Path)
|
|
parser.add_argument("resource_hash", type=parse_hash)
|
|
parser.add_argument("outpath", type=Path)
|
|
args = parser.parse_args()
|
|
|
|
with args.archive.open("rb") as archive:
|
|
header = archive.read(40)
|
|
if len(header) != 40:
|
|
raise SystemExit("archive header is too short")
|
|
magic, _version, index_pos, _index_size, *_rest = struct.unpack("<IIQIQIQ", header)
|
|
if magic != 1380009042:
|
|
raise SystemExit(f"unexpected archive magic: {magic}")
|
|
|
|
archive.seek(index_pos + 16)
|
|
file_count, segment_count, _dependency_count = struct.unpack("<III", archive.read(12))
|
|
|
|
found: tuple[int, int] | None = None
|
|
for _ in range(file_count):
|
|
entry = archive.read(FILE_ENTRY_SIZE)
|
|
(
|
|
key,
|
|
_timestamp,
|
|
_flags,
|
|
segments_start,
|
|
segments_end,
|
|
_dependencies_start,
|
|
_dependencies_end,
|
|
_sha1,
|
|
) = struct.unpack("<QqIIIII20s", entry)
|
|
if key == args.resource_hash:
|
|
if segments_end - segments_start != 1:
|
|
raise SystemExit(
|
|
f"resource has {segments_end - segments_start} segments; expected 1"
|
|
)
|
|
found = (segments_start, segments_end)
|
|
|
|
if found is None:
|
|
raise SystemExit(f"resource hash {args.resource_hash} not found")
|
|
|
|
segments = []
|
|
for _ in range(segment_count):
|
|
segments.append(struct.unpack("<QII", archive.read(FILE_SEGMENT_SIZE)))
|
|
|
|
offset, z_size, size = segments[found[0]]
|
|
archive.seek(offset)
|
|
data = archive.read(z_size)
|
|
if len(data) != z_size:
|
|
raise SystemExit("archive ended before segment data was fully read")
|
|
|
|
args.outpath.parent.mkdir(parents=True, exist_ok=True)
|
|
args.outpath.write_bytes(data)
|
|
print(f"wrote {args.outpath} z_size={z_size} size={size}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|