1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
|
# SPDX-FileCopyrightText: 2026 Tomasz Kramkowski <tomasz@kramkow.ski>
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import hashlib
import sys
import tempfile
import tomllib
from base64 import b85encode
from importlib.resources import files
from io import BytesIO
from json import dumps
from pathlib import Path
from typing import Annotated, BinaryIO, Self
from zlib import compress
from pydantic import BaseModel, ConfigDict, Field, field_validator
from .elf import Hook, compile_hooks
NonnegativeInt = Annotated[int, Field(strict=True, ge=0)]
class Compilation(BaseModel):
model_config = ConfigDict(extra="forbid")
sources: list[Path] = Field(min_length=1)
imports: dict[str, NonnegativeInt] = Field(default_factory=dict)
hooks: list[Hook] = Field(min_length=1)
class FilePatch(BaseModel):
model_config = ConfigDict(val_json_bytes="hex", extra="forbid")
path: Path
sha256: bytes = Field(min_length=32, max_length=32)
edits: list[tuple[NonnegativeInt, bytes]]
compiled: Compilation | None = None
class PatchSet(BaseModel):
model_config = ConfigDict(extra="forbid")
target: str
targets: list[FilePatch]
@field_validator("targets")
@classmethod
def unique_paths(cls, targets: list[FilePatch]) -> list[FilePatch]:
paths = [target.path for target in targets]
if len(set(paths)) != len(paths):
raise ValueError("duplicate patch target")
return targets
@classmethod
def parse(cls, config: dict, directory: Path) -> Self:
result = cls.model_validate(config)
for target in result.targets:
if target.compiled is not None:
target.compiled.sources = [
directory / source for source in target.compiled.sources
]
return result
def compile_patch(config: FilePatch, source: BinaryIO, output: Path) -> dict:
source.seek(0)
data = source.read()
if hashlib.sha256(data).digest() != config.sha256:
raise ValueError(f"input SHA-256 mismatch: {config.path}")
edits = list(config.edits)
if config.compiled is not None:
compiled = config.compiled
edits += compile_hooks(
source, compiled.sources, compiled.imports, compiled.hooks, output
)
end = 0
for offset, replacement in sorted(edits):
if offset < end:
raise ValueError("overlapping edits")
end = offset + len(replacement)
patched = BytesIO(data)
for offset, replacement in edits:
patched.seek(offset)
patched.write(replacement)
return {
"original_hash": config.sha256,
"patched_hash": hashlib.sha256(patched.getbuffer()).digest(),
"edits": edits,
}
def render(patches: dict[str, dict]) -> str:
template = files("steamvr_patches").joinpath("template.py").read_text()
placeholder = "PATCH_DATA = {}"
if template.count(placeholder) != 1:
raise ValueError("missing or duplicate runtime patch-table placeholder")
data = dumps(patches, default=bytes.hex, separators=(",", ":")).encode()
encoded = b85encode(compress(data, level=9))
return template.replace(placeholder, f"PATCH_DATA = decode_patch_data({encoded!r})")
def generate(patch_data: Path, input_root: Path) -> str:
with (patch_data / "patches.toml").open("rb") as source:
definition = PatchSet.parse(tomllib.load(source), patch_data)
patches = {}
with tempfile.TemporaryDirectory(prefix="generate-patcher-") as temporary:
for index, config in enumerate(definition.targets):
with (input_root / config.path).open("rb") as source:
patches[config.path.as_posix()] = compile_patch(
config, source, Path(temporary) / str(index)
)
return render(patches)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"patch_data", type=Path, help="directory containing patches.toml etc"
)
parser.add_argument(
"input_root", type=Path, help="directory containing the unpatched target files"
)
args = parser.parse_args()
sys.stdout.write(generate(args.patch_data, args.input_root))
|