Files
ghostwire/build_whatsapp_bundle.py

101 lines
4.1 KiB
Python

"""Build a deterministic single Python file; run this only for development."""
import base64
import hashlib
import io
import json
import lzma
from pathlib import Path
import textwrap
import zipfile
ROOT = Path(__file__).resolve().parent
BOOTSTRAP = '''#!/usr/bin/env python3
"""Portable WhatsApp suite: python whatsapp.py --help
All Python modules, native helpers and regression fixtures are embedded.
Only native DLLs are cached beside this file when a live command needs them.
Core commands use the standard library. Legacy CDP/Frida/QR commands retain
their optional dependencies. Live WebView probes require the supported
WhatsApp Desktop build, Windows x64 and 64-bit Python 3.11+.
Generated by build_whatsapp_bundle.py; edit the development sources to rebuild.
"""
import base64
import hashlib
import importlib.abc
import importlib.util
import io
import lzma
from pathlib import Path
import sys
import zipfile
PAYLOAD_SHA256 = __DIGEST__
PAYLOAD = (
__PAYLOAD__
)
def main():
compressed = base64.b85decode(PAYLOAD)
if hashlib.sha256(compressed).hexdigest() != PAYLOAD_SHA256:
raise RuntimeError('Bundle integrity check failed')
archive = zipfile.ZipFile(io.BytesIO(lzma.decompress(compressed)))
members = frozenset(archive.namelist())
launcher = Path(__file__).resolve()
class EmbeddedModules(importlib.abc.MetaPathFinder, importlib.abc.Loader):
def find_spec(self, fullname, path=None, target=None):
relative = fullname.replace('.', '/')
for name, package in ((relative + '/__init__.py', True), (relative + '.py', False)):
if name in members:
return importlib.util.spec_from_loader(fullname, self, is_package=package)
return None
def create_module(self, spec):
return None
def exec_module(self, module):
relative = module.__name__.replace('.', '/')
name = relative + ('/__init__.py' if module.__spec__.submodule_search_locations is not None else '.py')
module.__file__ = str(launcher) + '!' + name
exec(compile(archive.read(name), module.__file__, 'exec'), module.__dict__)
sys._whatsapp_bundle = {'base': launcher.parent, 'launcher': launcher, 'read': archive.read}
sys.meta_path.insert(0, EmbeddedModules())
from whatsapp_suite.cli import main as cli
return cli()
if __name__ == '__main__':
raise SystemExit(main())
'''
def main():
files={p.relative_to(ROOT).as_posix():p.read_bytes() for p in sorted((ROOT/'whatsapp_suite').glob('*.py'))}
for name in ('math_probe.dll','search_probe.dll','sqlite_inject.js','webhook_probe.dll','webhook_listener.js'):
files['whatsapp_suite/assets/'+name]=(ROOT/'whatsapp_suite/assets'/name).read_bytes()
files['whatsapp_suite/_regression.py']=(ROOT/'tests/test_whatsapp_suite.py').read_bytes()
for p in sorted((ROOT/'tests').glob('*.cjs')):
files['tests/'+p.name]=p.read_bytes()
# Whole-archive compression shares repeated source and native-helper bytes.
memory=io.BytesIO()
with zipfile.ZipFile(memory,'w',compression=zipfile.ZIP_STORED) as archive:
for name,data in sorted(files.items()):
entry=zipfile.ZipInfo(name,date_time=(1980,1,1,0,0,0))
archive.writestr(entry,data)
payload=lzma.compress(memory.getvalue(),preset=9)
encoded=base64.b85encode(payload).decode('ascii')
lines='\n'.join(' '+repr(line) for line in textwrap.wrap(encoded,100,break_on_hyphens=False))
source=BOOTSTRAP.replace('__DIGEST__',repr(hashlib.sha256(payload).hexdigest())).replace('__PAYLOAD__',lines)
destination=ROOT/'whatsapp.py'
destination.write_text(source,encoding='utf-8',newline='\n')
report={'bundle':destination.name,'bundle_bytes':destination.stat().st_size,'embedded_source_and_asset_bytes':sum(map(len,files.values())),'files_embedded':len(files),'sha256':hashlib.sha256(destination.read_bytes()).hexdigest()}
(ROOT/'analysis/whatsapp-suite').mkdir(parents=True, exist_ok=True)
(ROOT/'analysis/whatsapp-suite/bundle-build.json').write_text(json.dumps(report,indent=2),encoding='utf-8')
print(json.dumps(report))
if __name__=='__main__':main()