-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathpopulate_versions.py
More file actions
367 lines (286 loc) · 14 KB
/
Copy pathpopulate_versions.py
File metadata and controls
367 lines (286 loc) · 14 KB
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
#!/usr/bin/env python3
"""
Script to populate keys_versions.py with iOS version information.
This script reads:
- versions/version-*.txt: Cumulative snapshots of keys that exist in each iOS version
- mapping-gestalt.h: All known gestalt keys
The script determines when each key was first introduced and when it was removed
(if applicable) by comparing consecutive version snapshots.
Keys are annotated with:
- "8.4": First appeared in iOS 8.4 (earliest tracked version)
- "12.0": First appeared in iOS 12.0
- Removed keys are tracked separately with the version they were removed in
"""
from pathlib import Path
import re
def extract_hashes_from_mapping(mapping_file: Path) -> set[str]:
"""Extract all obfuscated key hashes from a mapping file."""
hashes = set()
with mapping_file.open('r') as f:
for line in f:
# Match lines like: "hash", "key_name",
# or: "hash", NULL,
match = re.match(r'\s*"([^"]+)",\s*(?:"[^"]*"|NULL),?', line)
if match:
hashes.add(match.group(1))
return hashes
def read_version_file(version_file: Path) -> set[str]:
"""Read hashes from a version-specific file (e.g., versions/version-26.0.txt)."""
hashes = set()
if not version_file.exists():
print(f"Warning: {version_file} does not exist")
return hashes
with version_file.open('r') as f:
for line in f:
hash_str = line.strip()
if hash_str:
hashes.add(hash_str)
return hashes
def sort_version_file(version_file: Path) -> None:
"""Sort a version file alphabetically (case-insensitive) and remove duplicates."""
if not version_file.exists():
return
# Read all lines
with version_file.open('r') as f:
lines = [line.strip() for line in f if line.strip()]
# Sort uniquely (case-insensitive like sort -u)
sorted_lines = sorted(set(lines), key=str.lower)
# Write back
with version_file.open('w') as f:
for line in sorted_lines:
f.write(f'{line}\n')
def parse_version(version_str: str) -> tuple:
"""Parse version string like '12.0' or '26.2' into tuple (12, 0) or (26, 2)."""
parts = version_str.split('.')
return tuple(int(p) for p in parts)
def generate_keys_versions():
"""Generate keys_versions.py from existing data."""
# File paths
mapping_file = Path('mapping.h') # Contains ALL keys including non-gestalt
mapping_legacy = Path('mapping-legacy.h')
output_file = Path('keys_versions.py')
# Extract all hashes from mapping files
print(f"Reading hashes from {mapping_file}...")
main_hashes = extract_hashes_from_mapping(mapping_file)
print(f"Found {len(main_hashes)} hashes in mapping.h")
legacy_hashes = set()
if mapping_legacy.exists():
print(f"Reading hashes from {mapping_legacy}...")
legacy_hashes = extract_hashes_from_mapping(mapping_legacy)
print(f"Found {len(legacy_hashes)} hashes in mapping-legacy.h")
all_hashes = main_hashes | legacy_hashes
print(f"Total mapped keys: {len(all_hashes)}\n")
# Auto-discover all version files in versions/ directory
versions_dir = Path('versions')
if versions_dir.exists():
# Exclude version-stats.txt and version-sim.txt from the glob results
version_files = sorted([f for f in versions_dir.glob('version-*.txt')
if f.name not in ['version-stats.txt', 'version-sim.txt']])
else:
version_files = []
# Read simulator keys separately
sim_file = versions_dir / 'version-sim.txt' if versions_dir.exists() else None
simulator_keys = set()
if sim_file and sim_file.exists():
sort_version_file(sim_file)
simulator_keys = read_version_file(sim_file)
print(f"Reading {sim_file}: {len(simulator_keys)} simulator-only keys (sorted)\n")
if not version_files:
print("Warning: No version files found in versions/ directory")
print("Expected format: versions/version-26.0.txt, versions/version-26.2.txt, etc.")
# Read all version files and sort by version number
version_data = []
for version_file in version_files:
version_str = version_file.stem.replace('version-', '')
# Auto-sort the version file
sort_version_file(version_file)
hashes = read_version_file(version_file)
print(f"Reading {version_file}: {len(hashes)} hashes (sorted)")
version_data.append((version_str, parse_version(version_str), hashes))
# Sort by version number
version_data.sort(key=lambda x: x[1])
print()
# Build version mapping by finding when each key first appeared
version_map: dict[str, str] = {} # hash -> version introduced
removed_map: dict[str, str] = {} # hash -> version removed
reintroduced_map: dict[str, list[str]] = {} # hash -> list of versions where it was reintroduced
# Track all keys we've ever seen
all_seen_keys: set[str] = set()
# Track first and last appearance of each key
first_appearance: dict[str, int] = {} # hash -> index of first appearance
last_appearance: dict[str, int] = {} # hash -> index of last appearance
# First pass: find all appearances
for i, (version_str, _, hashes) in enumerate(version_data):
all_seen_keys.update(hashes)
for hash_str in hashes:
if hash_str not in first_appearance:
first_appearance[hash_str] = i
last_appearance[hash_str] = i
# Second pass: build version map and detect gaps (removed/reintroduced)
for hash_str in all_seen_keys:
first_idx = first_appearance[hash_str]
last_idx = last_appearance[hash_str]
# Mark with first appearance version
version_map[hash_str] = version_data[first_idx][0]
# Check for gaps between first and last appearance
reintroductions = []
was_present = True
for i in range(first_idx + 1, last_idx + 1):
is_present = hash_str in version_data[i][2]
if not was_present and is_present:
# Key reappeared - this is a reintroduction
reintroductions.append(version_data[i][0])
was_present = is_present
if reintroductions:
reintroduced_map[hash_str] = reintroductions
# Third pass: find permanently removed keys
# A key is "removed" if it doesn't appear in the last version but appeared earlier
last_version_hashes = version_data[-1][2] if version_data else set()
for hash_str in all_seen_keys:
if hash_str not in last_version_hashes:
# Find the last version it appeared in
last_idx = last_appearance[hash_str]
# Mark it as removed in the next version (if there is one)
if last_idx < len(version_data) - 1:
removed_map[hash_str] = version_data[last_idx + 1][0]
# Keys in mapping files but not in any version file
unmapped_keys = all_hashes - all_seen_keys
# Mark simulator keys first
simulator_only = unmapped_keys & simulator_keys
if simulator_only:
print(f"Found {len(simulator_only)} simulator-only keys")
for hash_str in simulator_only:
version_map[hash_str] = "sim"
# Remaining unmapped keys are truly unknown
truly_unknown = unmapped_keys - simulator_keys
if truly_unknown:
print(f"Found {len(truly_unknown)} mapped keys not in any version file (marking as unknown)")
for hash_str in truly_unknown:
version_map[hash_str] = "unknown"
# Keys in version files but not mapped yet
unknown_keys = all_seen_keys - all_hashes
# Always write the unmapped keys file (even if empty)
unmapped_file = Path('unmapped-keys-from-versions.txt')
with unmapped_file.open('w') as f:
f.write(f'# Keys found in version files but not in mapping.h or mapping-legacy.h\n')
f.write(f'# Total: {len(unknown_keys)} unmapped keys\n')
f.write(f'# These keys need to be deobfuscated and added to an appropriate mapping file\n\n')
for h in sorted(unknown_keys):
f.write(f'{h}\n')
if unknown_keys:
print(f"Warning: Found {len(unknown_keys)} keys in version files but not in mapping files")
print(f"These unmapped keys are saved in unmapped-keys-from-versions.txt")
# Print statistics
version_stats: dict[str, int] = {}
for version in version_map.values():
version_stats[version] = version_stats.get(version, 0) + 1
# Sort for consistent output
sorted_hashes = sorted(version_map.keys())
# Generate the Python file
print(f"\nGenerating {output_file}...")
with output_file.open('w') as f:
f.write('"""iOS version information for MobileGestalt keys.\n\n')
f.write('This file is auto-generated by populate_versions.py.\n')
f.write('Do not edit manually.\n\n')
f.write('Keys are annotated with the iOS version they first appeared in.\n')
f.write('- "X.Y": First appeared in iOS X.Y\n')
f.write('- "sim": Simulator-only key (not found in any physical iOS version)\n')
f.write('- "unknown": Not found in any tracked version\n')
f.write('Removed keys are tracked in KEY_IOS_REMOVED with the version they were removed.\n')
f.write('Reintroduced keys are tracked in KEY_IOS_REINTRODUCED with versions they came back.\n')
f.write('"""\n\n')
f.write('# Dictionary mapping obfuscated key hash -> iOS version introduced\n')
f.write('KEY_IOS_VERSIONS = {\n')
for hash_str in sorted_hashes:
version = version_map[hash_str]
f.write(f' "{hash_str}": "{version}",\n')
f.write('}\n')
# Write removed keys dictionary
f.write('\n# Dictionary mapping obfuscated key hash -> iOS version removed\n')
f.write('# Only includes keys that were present in an earlier version but removed later\n')
f.write('KEY_IOS_REMOVED = {\n')
for hash_str in sorted(removed_map.keys()):
removed_version = removed_map[hash_str]
f.write(f' "{hash_str}": "{removed_version}",\n')
f.write('}\n')
# Write reintroduced keys dictionary
f.write('\n# Dictionary mapping obfuscated key hash -> list of iOS versions where it was reintroduced\n')
f.write('# These keys disappeared in some versions but came back later\n')
f.write('KEY_IOS_REINTRODUCED = {\n')
for hash_str in sorted(reintroduced_map.keys()):
versions_list = reintroduced_map[hash_str]
versions_str = ', '.join(f'"{v}"' for v in versions_list)
f.write(f' "{hash_str}": [{versions_str}],\n')
f.write('}\n')
# Print statistics
print(f"\n✓ Generated {output_file}")
print(f" - Total keys: {len(version_map)}")
if removed_map:
print(f" - Removed keys tracked: {len(removed_map)}")
if reintroduced_map:
print(f" - Reintroduced keys tracked: {len(reintroduced_map)}")
# Sort and print version stats
def sort_key(v):
if v.startswith('post-'):
return (999, 999)
try:
return parse_version(v)
except:
return (998, 998)
# Generate version statistics report
report_file = Path('versions/version-stats.txt')
print(f"\n✓ Generating {report_file}")
with report_file.open('w') as f:
f.write('MobileGestalt Keys - iOS Version Statistics\n')
f.write('=' * 60 + '\n')
f.write('Generated by populate_versions.py\n\n')
f.write(f'Total unique keys tracked: {len(version_map)}\n')
if removed_map:
f.write(f'Total removed keys: {len(removed_map)}\n')
if reintroduced_map:
f.write(f'Total reintroduced keys: {len(reintroduced_map)}\n')
f.write('\n')
f.write('Keys per iOS Version:\n')
f.write('-' * 60 + '\n')
# Calculate cumulative count for each version
cumulative_counts = {}
for i, (version_str, _, hashes) in enumerate(version_data):
cumulative_counts[version_str] = len(hashes)
# Print version stats to file - iterate over ALL versions, not just those with new keys
for version_str in sorted(cumulative_counts.keys(), key=sort_key):
cumulative_count = cumulative_counts[version_str]
new_count = version_stats.get(version_str, 0)
if new_count > 0:
f.write(f'iOS {version_str:8s} {cumulative_count:4d} total keys ({new_count:+4d} new)\n')
else:
f.write(f'iOS {version_str:8s} {cumulative_count:4d} total keys\n')
# Print removed keys summary if any
if removed_map:
f.write('\n')
f.write('Removed Keys by Version:\n')
f.write('-' * 60 + '\n')
removed_stats: dict[str, int] = {}
for removed_version in removed_map.values():
removed_stats[removed_version] = removed_stats.get(removed_version, 0) + 1
for version_str in sorted(removed_stats.keys(), key=sort_key):
count = removed_stats[version_str]
f.write(f'iOS {version_str:8s} {count:4d} keys removed\n')
# Print reintroduced keys summary if any
if reintroduced_map:
f.write('\n')
f.write('Reintroduced Keys by Version:\n')
f.write('-' * 60 + '\n')
reintro_stats: dict[str, int] = {}
for versions_list in reintroduced_map.values():
for version_str in versions_list:
reintro_stats[version_str] = reintro_stats.get(version_str, 0) + 1
for version_str in sorted(reintro_stats.keys(), key=sort_key):
count = reintro_stats[version_str]
f.write(f'iOS {version_str:8s} {count:4d} keys reintroduced\n')
print(f" - Report saved to {report_file}")
# Print to console
for version_str in sorted(version_stats.keys(), key=sort_key):
count = version_stats[version_str]
print(f" - iOS {version_str}: {count} keys")
if __name__ == '__main__':
generate_keys_versions()