Splice by joining bytes around the original module code
A spliced module is now the original's header, a prefix, the original's marshalled code verbatim, and a suffix. The original comes first in the stream, so its interned-string references stay valid; the wrapper and the overrides are marshalled with version 0, which doesn't intern, so the prefix and suffix don't depend on the original's bytes. A patcher can splice without unmarshalling, and IMVU's code is no longer re-marshalled at all. The helper checks every spliced module round-trips. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
17527688f9
commit
63a4e31bc0
1 file changed
+39
-13
+39
-13
@@ -8,8 +8,9 @@ with the class's own name, so private names mangle correctly, and are then copie
|
||||
|
||||
Input on stdin, JSON: {"library": <dir of patched sources>, "modules": [{"path": "imvu/account.py",
|
||||
"base": <base64>, "patched": <base64>, "pyo": <base64>}]}
|
||||
Output on stdout, JSON: {"modules": [{"path", "pyo": <base64> or null, "overrides": [...], "errors": [...],
|
||||
"warnings": [...]}]}
|
||||
Output on stdout, JSON: {"modules": [{"path", "pyo", "prefix", "suffix": <base64> or null, "overrides": [...],
|
||||
"errors": [...], "warnings": [...]}]}. The spliced pyo is the original's 8-byte header + prefix + the original's
|
||||
marshalled code (bytes 8 onward, verbatim) + suffix; see wrapper().
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
@@ -47,6 +48,8 @@ def %(save)s(namespace, names):
|
||||
namespace['%(prefix)s' + name] = namespace[name]
|
||||
''' % {'patch_class': PATCH_CLASS, 'save': SAVE_ORIGINALS, 'prefix': ORIGINAL_PREFIX}
|
||||
STUB = "exec 'T5DE_ORIGINAL'\nexec 'T5DE_OVERRIDES'\n"
|
||||
# Stands in for the original module code while marshalling the wrapper; replaced by the original's bytes.
|
||||
ORIGINAL_MARKER = '\0t5de: original module code\0'
|
||||
|
||||
|
||||
class SpliceError(Exception):
|
||||
@@ -279,15 +282,34 @@ def splice(path, base_source, patched_source, pyo):
|
||||
original = marshal.loads(pyo[8:])
|
||||
tree = override_tree(units, patched_source)
|
||||
overrides = compile(tree, original.co_filename, 'exec', original.co_flags & FUTURE_FLAGS, True)
|
||||
stub = compile(STUB, original.co_filename, 'exec', 0, True)
|
||||
consts = tuple(original if c == 'T5DE_ORIGINAL' else overrides if c == 'T5DE_OVERRIDES' else c
|
||||
prefix, suffix = wrapper(original.co_filename, overrides)
|
||||
spliced = pyo[:8] + prefix + pyo[8:] + suffix
|
||||
embedded = [c for c in marshal.loads(spliced[8:]).co_consts if isinstance(c, types.CodeType)]
|
||||
assert len(embedded) == 2 and marshal.dumps(embedded[0]) == marshal.dumps(original)
|
||||
# Marshal version 0 drops which strings were interned, so compare the overrides by content, not bytes.
|
||||
assert same_code(embedded[1], overrides)
|
||||
names = ['%s.%s' % (class_name, name) if class_name else name for _, _, class_name, name in units]
|
||||
warnings = warnings + fidelity_warnings(original, base_source, patched_source, names)
|
||||
return (prefix, suffix), names, errors, warnings, changed_functions
|
||||
|
||||
|
||||
def wrapper(filename, overrides):
|
||||
"""The bytes around IMVU's original module code in a spliced .pyo: header + prefix + original + suffix.
|
||||
|
||||
The module code object runs the original module code, then the overrides. The original is embedded as its
|
||||
marshalled bytes, verbatim, and comes first in the stream, so its interned-string references stay valid; the
|
||||
wrapper is marshalled with version 0, which doesn't intern (names are interned again when the code loads). So
|
||||
the prefix and suffix don't depend on the original's bytes, and a patcher can splice without unmarshalling."""
|
||||
stub = compile(STUB, filename, 'exec', 0, True)
|
||||
consts = tuple(ORIGINAL_MARKER if c == 'T5DE_ORIGINAL' else overrides if c == 'T5DE_OVERRIDES' else c
|
||||
for c in stub.co_consts)
|
||||
module = types.CodeType(stub.co_argcount, stub.co_nlocals, stub.co_stacksize, stub.co_flags, stub.co_code,
|
||||
consts, stub.co_names, stub.co_varnames, stub.co_filename, stub.co_name,
|
||||
stub.co_firstlineno, stub.co_lnotab, stub.co_freevars, stub.co_cellvars)
|
||||
names = ['%s.%s' % (class_name, name) if class_name else name for _, _, class_name, name in units]
|
||||
warnings = warnings + fidelity_warnings(original, base_source, patched_source, names)
|
||||
return pyo[:8] + marshal.dumps(module), names, errors, warnings, changed_functions
|
||||
data, marker = marshal.dumps(module, 0), marshal.dumps(ORIGINAL_MARKER, 0)
|
||||
assert data.count(marker) == 1
|
||||
prefix, suffix = data.split(marker)
|
||||
return prefix, suffix
|
||||
|
||||
|
||||
def code_objects(module):
|
||||
@@ -394,14 +416,18 @@ def main():
|
||||
results, changed = [], {}
|
||||
for module in request['modules']:
|
||||
path = module['path']
|
||||
result = {'path': path, 'pyo': None, 'overrides': [], 'errors': [], 'warnings': []}
|
||||
result = {'path': path, 'pyo': None, 'prefix': None, 'suffix': None, 'overrides': [], 'errors': [],
|
||||
'warnings': []}
|
||||
try:
|
||||
pyo, names, errors, warnings, changed_functions = splice(
|
||||
path, base64.b64decode(module['base']), base64.b64decode(module['patched']),
|
||||
base64.b64decode(module['pyo']))
|
||||
pyo = base64.b64decode(module['pyo'])
|
||||
parts, names, errors, warnings, changed_functions = splice(
|
||||
path, base64.b64decode(module['base']), base64.b64decode(module['patched']), pyo)
|
||||
result.update(overrides=names, errors=errors, warnings=warnings)
|
||||
if pyo is not None:
|
||||
result['pyo'] = base64.b64encode(pyo).decode('ascii')
|
||||
if parts is not None:
|
||||
prefix, suffix = parts
|
||||
result['prefix'] = base64.b64encode(prefix).decode('ascii')
|
||||
result['suffix'] = base64.b64encode(suffix).decode('ascii')
|
||||
result['pyo'] = base64.b64encode(pyo[:8] + prefix + pyo[8:] + suffix).decode('ascii')
|
||||
if changed_functions:
|
||||
changed[module_name(path)] = (path, changed_functions)
|
||||
except SpliceError as e:
|
||||
|
||||
Reference in new issue
Block a user