Files

96 lines
4.9 KiB
Python

"""Read actual APK AudioClip payloads; never modify the APK or project assets."""
import argparse
import hashlib
import importlib.metadata
import io
import json
from pathlib import Path, PurePosixPath
import re
import wave
import uuid
import zipfile
import UnityPy
def sha(data):
return hashlib.sha256(data).hexdigest()
def require(condition, message):
if not condition:
raise ValueError(message)
def pcm(wav_bytes):
with wave.open(io.BytesIO(wav_bytes)) as wav:
return ((wav.getnchannels(), wav.getsampwidth(), wav.getframerate(), wav.getnframes()),
wav.readframes(wav.getnframes()))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--apk', required=True)
parser.add_argument('--project', required=True)
parser.add_argument('--evidence', required=True)
args = parser.parse_args()
project, apk, evidence = Path(args.project), Path(args.apk), Path(args.evidence)
evidence.mkdir(parents=True, exist_ok=True)
version = importlib.metadata.version('UnityPy')
require(version == '1.25.4', f'Expected pinned QA inspector1.25.4, got {version}')
adoption = json.loads((project / 'SourceAssets/Audio/Voice/active-adoption.json').read_text(encoding='utf-8-sig'))
require(len(adoption['clips']) == 44 and len({c['id'] for c in adoption['clips']}) == 44
and adoption['approvedCount'] == 44 and adoption['draftCount'] == 0, '44 unique approved adoption IDs required')
adopted = {c['id']: c for c in adoption['clips']}
extracted = evidence / ('apk-serialized-data-' + uuid.uuid4().hex)
extracted.mkdir()
record = {'apkSha256': sha(apk.read_bytes()), 'tool': 'UnityPy', 'toolVersion': version,
'method': 'Join actual APK split assets; deserialize AudioClips; decode PCM16; exact PCM SHA and format comparison to adopted WAV',
'status': 'failed', 'checks': [], 'extractedEntries': []}
try:
with zipfile.ZipFile(apk) as archive:
entries = [n for n in archive.namelist() if n.startswith('assets/bin/Data/')
and not n.endswith('/') and '/Managed/' not in n]
names = sorted({re.sub(r'\.split\d+$', '', n) for n in entries})
for name in names:
parts = sorted([n for n in entries if re.sub(r'\.split\d+$', '', n) == name],
key=lambda n: int(n.rsplit('.split', 1)[1]) if '.split' in n else -1)
if '.split' in parts[0]:
require([int(n.rsplit('.split', 1)[1]) for n in parts] == list(range(len(parts))), 'Contiguous split asset parts required')
data = b''.join(archive.read(n) for n in parts)
path = extracted / PurePosixPath(name).name
path.write_bytes(data)
record['extractedEntries'].append({'parts': parts, 'joinedFile': str(path), 'sha256': sha(data)})
clips = [obj.read() for obj in UnityPy.load(str(extracted)).objects if obj.type.name == 'AudioClip']
record['actualClipCount'] = len(clips)
require(len(clips) == 44 and len({clip.m_Name for clip in clips}) == 44, 'Actual APK must contain44 unique AudioClips')
require({clip.m_Name for clip in clips} == set(adopted), 'Actual APK clip IDs must exactly equal adoption IDs')
for clip in sorted(clips, key=lambda c: c.m_Name):
adoption_clip = adopted[clip.m_Name]
runtime = (project / adoption_clip['runtimeFile']).read_bytes()
require(sha(runtime) == adoption_clip['runtimeSha256'].lower(), 'Adopted runtime SHA mismatch: ' + clip.m_Name)
samples = clip.samples
require(len(samples) == 1, 'Exactly one PCM payload required: ' + clip.m_Name)
packed_wave = next(iter(samples.values()))
packed_format, packed_pcm = pcm(packed_wave)
runtime_format, runtime_pcm = pcm(runtime)
passed = (packed_format == runtime_format and packed_format[:3] == (1, 2, 44100)
and sha(packed_pcm) == sha(runtime_pcm) and clip.m_CompressionFormat == 0)
record['checks'].append({'id': clip.m_Name, 'passed': passed,
'runtimeSha256': sha(runtime), 'apkPcmSha256': sha(packed_pcm),
'runtimePcmSha256': sha(runtime_pcm), 'format': packed_format,
'resource': clip.m_Resource.m_Source, 'offset': clip.m_Resource.m_Offset,
'resourceSize': clip.m_Resource.m_Size})
require(passed, f'Actual packed voice PCM mismatch: {clip.m_Name}')
record['status'] = 'passed'
except Exception as exc:
record['error'] = str(exc)
raise
finally:
(evidence / 'apk-voice-payload.json').write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
print(f'Actual APK voice PCM exact matches: {len(record["checks"])}/44')
if __name__ == '__main__':
main()