From c472bd4a8c626c671f4be4d6e80033d5f14358ea Mon Sep 17 00:00:00 2001 From: sasheg dev <152582704+SashegDev@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:17:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B3=D0=BD=D0=B5=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BB:=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=BE=D0=BB=D0=BE=D0=B2=20=D1=81=20=D0=B2?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20=D0=B8=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D1=8B=20=D1=80=D0=B5=D0=B2=D0=B5=D1=80=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - weapons.csv: 23 ствола с уроном, множителем в голову, темпом, магазином, перезарядкой, разбросом (конус прицела), отдачей, типом патрона, верстаком и ценами исследования. Источники - официальная вики и community-вики, расхождения помечены в колонке source. Не хватает цены (2 из 23) и дальности (18 из 23). Границы реверса ремейка, чтобы не тратить на них время снова: - классы в game.jar помечены DE AD C0 DE и зашифрованы своим форматом, не однобайтовым XOR как ассеты; единственный незашифрованный класс - точка входа net/minecraft/client/main/RustexMain, в ней видны два статических дешифратора строк (qnbt.super и vyuhke.super), а сами классы зашифрованы; - REA не берет .class как цель (только архивы), Ghidra 12.1.4 стоит в D:\ghidra-tools и через analyzeHeadless импортирует распакованные классы; - главное: урона, отдачи и спрея огнестрела в клиенте нет вообще, это серверные данные. Клиент знает только магазин, тип патрона, зум ADS, прочность и reach. REA даст формулы, а не числа, поэтому числа берём с вики. Инструменты в tools/: inspect-constants.py (разбор constant pool класса) и find-decryptor.py (поиск маркера и слов дешифратора). План дополнен этими находками. --- PLAN.md | 24 +++- .../src/main/resources/items/weapons.csv | 24 ++++ tools/find-decryptor.py | 55 +++++++++ tools/inspect-constants.py | 109 ++++++++++++++++++ 4 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 rsx-protocol/src/main/resources/items/weapons.csv create mode 100644 tools/find-decryptor.py create mode 100644 tools/inspect-constants.py diff --git a/PLAN.md b/PLAN.md index 828cae8..91acba4 100644 --- a/PLAN.md +++ b/PLAN.md @@ -548,7 +548,9 @@ heavy plate helmet 90 % пули, hazmat 30/30; по футболке источ **Полный список предметов с характеристиками** — в `rsx-protocol/src/main/resources/items/items.csv` (218 шт, выгружено из ремейка, -п. 1.6). Названия и описания — в `assets/rustex/data/item-texts.json` (543 шт). +п. 1.6). Названия и описания — в `rsx-protocol/src/main/resources/items/item-texts.json` +(543 шт). Огнестрел — в `weapons.csv` (23 ствола: урон, темп, магазин, разброс, +отдача, патрон, верстак, цены исследования). ### 1.8 Строительные объекты и сокеты @@ -984,8 +986,10 @@ rsx-client\scripts\run-game.ps1 # клиент 4. **Шаг D** — конфиг, мир 4000×4000 один раз, барьер. 5. **Шаг E** — транспорт на NIO. 6. **Шаг F** — DistantView. -7. **REA** — найти в коде ремейка таблицу оружия по `rweaponid` (урон, отдача, спрей, - темп) и шифр конфигов `.obj/.rrs/.nbt`; до этого — цифры с официальной вики. +7. **REA** — код ремейка прочитать нельзя без взлома загрузчика (классы помечены + `DEADC0DE`), и урон/отдача/спрей огнестрела в клиенте всё равно серверные. + Числа взяты с вики (`weapons.csv`). Если формулы понадобятся — импортировать + распакованные классы в Ghidra headless и искать дешифратор. ### 6.4 Мелочи, которые легко упустить @@ -1000,6 +1004,20 @@ rsx-client\scripts\run-game.ps1 # клиент - **База предметов** — `rsx-protocol/src/main/resources/items/items.csv` (218 шт, из `models/stacks.json`), описания — `assets/rustex/data/item-texts.json` (543 шт), звуки — `rsx-protocol/src/main/resources/items/sounds.csv` (505 событий). +- **Огнестрел** — `rsx-protocol/src/main/resources/items/weapons.csv` (23 ствола): + урон, множитель в голову, темп, магазин, перезарядка, разброс (конус прицела), + отдача, тип патрона, верстак, цены исследования. Собрано с официальной вики и + community-вики; расхождения помечены в колонке `source`. +- ⚠️ **Код ремейка прочитать нельзя без взлома.** Классы в `game.jar` помечены + `DE AD C0 DE` и зашифрованы своим форматом (не однобайтовым XOR, как ассеты). + Единственный незашифрованный класс — точка входа + `net/minecraft/client/main/RustexMain`; в ней видны два статических дешифратора + строк (`qnbt.super`, `vyuhke.super`), а сами классы зашифрованы. REA берёт + архивы, но не `.class`; Ghidra 12.1.4 стоит (`D:\ghidra-tools`), и через + `analyzeHeadless` можно импортировать распакованные классы. + **И важнее:** урон, отдача и спрей огнестрела в клиенте **отсутствуют** — это + серверные данные, в клиенте есть только магазин, тип патрона, зум ADS, + прочность и reach. То есть REA даст формулы, а не числа. Числа — из вики. - Пути: `G:\RustNotMC` (репозиторий), `G:\decompiled-rustex` (референс, только читать), `G:\rustexremake` (ремейк: `gamefiles\res`, `gamefiles\game.jar`, `working_dir`). - ⚠️ **`G:\decompiled-rustex` — НЕ источник фактов.** Он копиял community-вику 2017–2020 diff --git a/rsx-protocol/src/main/resources/items/weapons.csv b/rsx-protocol/src/main/resources/items/weapons.csv new file mode 100644 index 0000000..cf13eff --- /dev/null +++ b/rsx-protocol/src/main/resources/items/weapons.csv @@ -0,0 +1,24 @@ +id,name,damage,headshot,fire_rate,magazine,reload,range,accuracy,recoil,ammo_type,price,bench,blueprint,source +m92pistol,M92 Pistol,45,x2,400 RPM,15,2.39 sec,90 m,aim cone 1 deg / accuracy 75%,medium,Pistol Bullet (also HV + incendiary),250 scrap (bandit camp),,,official=https://wiki.facepunch.com/rust/item/pistol.m92 ; community=https://rustwiki.net/items/m92-pistol ; fandom=https://rust.fandom.com/wiki/M92_Pistol ; conflicts: headshot official x2 vs community 1.5x ; recoil community medium vs fandom very low ; price 250 scrap from fandom ; official lists weapon as not craftable and not researchable +revolver,Revolver,35,x2,343 RPM,8,3.4 sec,,aim cone 0.75 deg / accuracy 65%,low,Pistol Bullet (also HV + incendiary),,Workbench Level 1,Research Table 30 scrap ; Tech Tree 125 scrap (bench level 1),official=https://wiki.facepunch.com/rust/item/pistol.revolver ; community=https://rustwiki.net/items/revolver ; fandom=https://rust.fandom.com/wiki/Revolver ; conflicts: headshot official x2 vs community 1.5x ; range not documented +pythonrevolver,Python Revolver,55,x2,400 RPM,6,3.75 sec,72 m,aim cone 0.5 deg / accuracy 80%,medium,Pistol Bullet (also HV + incendiary),,Workbench Level 2,Research Table 60 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/pistol.python ; community=https://rustwiki.net/items/python-revolver ; fandom=https://rust.fandom.com/wiki/Python_Revolver ; conflicts: headshot official x2 vs community 1.5x ; recoil community medium vs fandom high ; blueprint fandom says 125 scrap research vs official 60 + 250 ; range 72 m from fandom only +eokapistol,Eoka Pistol,80,x2,30 RPM,1,2 sec,Short,aim cone 2 deg / accuracy 20%,very high,12 Gauge Shell (handmade / buckshot / slug / incendiary),,None (no workbench needed),Research Table 15 scrap ; blueprint known by default (Industrial Crafter),official=https://wiki.facepunch.com/rust/item/pistol.eoka ; community=https://rustwiki.net/items/eoka-pistol ; fandom=https://rust.fandom.com/wiki/Eoka_Pistol ; conflicts: damage official 80 (12 gauge slug) vs community 180 (total per shell) ; fire rate community 30 RPM vs official DPS 53.3 (slug) = 40 RPM ; recoil community very high vs fandom high ; range Short from fandom only +semiautomaticpistol,Semi-Automatic Pistol,40,x2,400 RPM,10,2.9 sec,Low,aim cone 0.75 deg / accuracy 70%,low,Pistol Bullet (also HV + incendiary),,Workbench Level 2,Research Table 60 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/pistol.semiauto ; community=https://rustwiki.net/items/semi-automatic-pistol ; fandom=https://rust.fandom.com/wiki/Semi_Automatic_Pistol ; conflicts: headshot official x2 vs community 1.5x ; recoil community low vs fandom medium ; range Low from fandom only +waterpipeshotgun,Waterpipe Shotgun,80,x2,13 RPM,1,4.5 sec,Short,aim cone 1 deg / accuracy 25%,very high,12 Gauge Shell (handmade / buckshot / slug),,Workbench Level 1,Research Table 30 scrap ; Tech Tree 125 scrap (bench level 1),official=https://wiki.facepunch.com/rust/item/shotgun.waterpipe ; community=https://rustwiki.net/items/waterpipe-shotgun ; fandom=https://rust.fandom.com/wiki/Waterpipe_Shotgun ; conflicts: damage official 80 (12 gauge slug; buckshot 15 per pellet / handmade 9 per pellet) vs community 180 (total) ; headshot official x2 vs community 1.5x ; reload community 4.5 sec vs fandom 6 sec ; fire rate community 13 RPM vs official DPS 40 (slug) = 30 RPM ; blueprint fandom says 100 scrap research vs official 30 + 125 +pumpshotgun,Pump Shotgun,80,x2,55 RPM,8,5.5 sec,10 m,aim cone 0 deg / accuracy 40%,high,12 Gauge Shell (handmade / buckshot / slug / incendiary),,Workbench Level 2,Research Table 60 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/shotgun.pump ; community=https://rustwiki.net/items/pump-shotgun ; fandom=https://rust.fandom.com/wiki/Pump_Shotgun ; conflicts: damage official 80 (12 gauge slug; buckshot 15 per pellet / handmade 9 per pellet) vs community 210 (total) ; magazine official description says 8 rounds vs fandom 6 ; headshot official x2 vs community 1.5x ; range 10 m from fandom only +doublebarrelshotgun,Double Barrel Shotgun,80,x2,120 RPM,2,5.5 sec,Short,aim cone 0.5 deg / accuracy 35%,very high,12 Gauge Shell (handmade / buckshot / slug / incendiary),,Workbench Level 1,Research Table 60 scrap ; Tech Tree 125 scrap (bench level 1),official=https://wiki.facepunch.com/rust/item/shotgun.double ; community=https://rustwiki.net/items/double-barrel-shotgun ; fandom=https://rust.fandom.com/wiki/Double_Barrel_Shotgun ; conflicts: damage official 80 (12 gauge slug; buckshot 15 per pellet / handmade 9 per pellet) vs community 180 (total) ; headshot official x2 vs community 1.5x ; range Short from fandom only +spas12shotgun,Spas-12 Shotgun,60,x2,240 RPM,6,5.8 sec,,aim cone 0 deg,,12 Gauge Shell (handmade / buckshot / slug / incendiary),,,,official=https://wiki.facepunch.com/rust/item/shotgun.spas12 ; community=https://rustwiki.net/items/spas-12-shotgun ; fandom=https://rust.fandom.com/wiki/Spas-12 ; conflicts: damage official 60 (12 gauge slug; buckshot 11.2 per pellet / handmade 6.8 per pellet) vs community 157.5 vs fandom 135 ; fire rate official DPS 240 (slug) = 240 RPM vs fandom 180 RPM ; headshot official x2 (slug) ; official lists weapon as not craftable and not researchable +nailgun,Nailgun,18,x1.5,400 RPM,16,3.1 sec,short (fires in an arc),aim cone 0.75 deg,,Nailgun Nails,,Workbench Level 1,Research Table 30 scrap ; blueprint known by default (Industrial Crafter),official=https://wiki.facepunch.com/rust/item/pistol.nailgun ; community=https://rustwiki.net/items/nailgun ; fandom=https://rust.fandom.com/wiki/Nailgun ; conflicts: recoil not documented by any source +boltactionrifle,Bolt Action Rifle,80,x2,35 RPM,4,5 sec,350-500 m,aim cone 0 deg / accuracy 92%,medium,5.56 Rifle Ammo (also HV + explosive + incendiary),,Workbench Level 3,Research Table 120 scrap ; Tech Tree 500 scrap (bench level 3),official=https://wiki.facepunch.com/rust/item/rifle.bolt ; community=https://rustwiki.net/items/bolt-action-rifle ; fandom=https://rust.fandom.com/wiki/Bolt_Action_Rifle ; conflicts: damage official 80 vs fandom body 75 (+15 bleed) ; recoil community medium vs fandom severe ; range 350-500 m from fandom only +huntingbow,Hunting Bow,50,x1.5,60 RPM,1,1 sec,~150 m,aim cone 1 deg / accuracy 75%,low,Arrow (wooden / bone / fire / high velocity),,None (no workbench needed),Research Table 120 scrap ; blueprint known by default (Industrial Crafter),official=https://wiki.facepunch.com/rust/item/bow.hunting ; community=https://rustwiki.net/items/hunting-bow ; fandom=https://rust.fandom.com/wiki/Hunting_Bow ; conflicts: headshot official x1.5 vs community 2x ; fire rate community 60 RPM vs official DPS 40 (wooden arrow) = 48 RPM +crossbow,Crossbow,60,x1.5,17 RPM,1,3.6 sec,~250 m,aim cone 1 deg / accuracy 80%,low,Arrow (wooden / bone / fire / high velocity),,Workbench Level 1,Research Table 30 scrap ; blueprint known by default (Industrial Crafter),official=https://wiki.facepunch.com/rust/item/crossbow ; community=https://rustwiki.net/items/crossbow ; fandom=https://rust.fandom.com/wiki/Crossbow ; conflicts: headshot official x1.5 vs community 2x ; fire rate community 17 RPM vs official DPS 60 (wooden arrow) = 60 RPM ; reload community 3.6 sec vs fandom 4.783 sec +hmlmg,HMLMG,57.5,x2,480 RPM,60,6.5 sec,,aim cone 0.2 deg,,5.56 Rifle Ammo (also HV + explosive + incendiary),,Workbench Level 3,Research Table 120 scrap ; Tech Tree 500 scrap (bench level 3),official=https://wiki.facepunch.com/rust/item/hmlmg ; community=https://rustwiki.net/items/hmlmg ; fandom has no page for this weapon ; conflicts: recoil and range not documented +thompson,Thompson,37.5,x2,462 RPM,20,4 sec,~250 m,aim cone 0.5 deg / accuracy 60%,medium,Pistol Bullet (also HV + incendiary),,Workbench Level 2,Research Table 60 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/smg.thompson ; community=https://rustwiki.net/items/thompson ; fandom=https://rust.fandom.com/wiki/Thompson ; conflicts: headshot official x2 vs community 1.5x ; recoil community medium vs fandom low/medium ; range ~250 m from fandom only +customsmg,Custom SMG,30,x2,600 RPM,30,4 sec,200 m,aim cone 0.5 deg / accuracy 55%,low,Pistol Bullet (also HV + incendiary),,Workbench Level 2,Research Table 60 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/smg.2 ; community=https://rustwiki.net/items/custom-smg ; fandom=https://rust.fandom.com/wiki/Custom_SMG ; conflicts: headshot official x2 vs community 1.5x ; damage official 30 vs fandom body 35 ; recoil community low vs fandom medium +mp5a4,MP5A4,37.5,x2,600 RPM,30,4 sec,72 m,aim cone 0.5 deg / accuracy 65%,medium,Pistol Bullet (also HV + incendiary),,Workbench Level 3,Research Table 120 scrap ; Tech Tree 500 scrap (bench level 3),official=https://wiki.facepunch.com/rust/item/smg.mp5 ; community=https://rustwiki.net/items/mp5a4 ; fandom=https://rust.fandom.com/wiki/MP5A4 ; conflicts: headshot official x2 vs community 1.5x ; range 72 m from fandom only ; fandom attackinterval 600 RPM agrees +assaultrifle,Assault Rifle,50,x2,450 RPM,30,4.4 sec,250-400 m,aim cone 0.2 deg / accuracy 62%,high,5.56 Rifle Ammo (also HV + explosive + incendiary),,Workbench Level 3,Research Table 120 scrap ; Tech Tree 500 scrap (bench level 3),official=https://wiki.facepunch.com/rust/item/rifle.ak ; community=https://rustwiki.net/items/assault-rifle ; fandom=https://rust.fandom.com/wiki/Assault_Rifle ; conflicts: damage official 50 vs fandom body 51 ; headshot official x2 vs fandom flat 100 ; range 250-400 m from fandom only +m39rifle,M39 Rifle,50,x2,343 RPM,20,3.25 sec,,aim cone 0.1 deg / accuracy 85%,medium,5.56 Rifle Ammo (also HV + explosive + incendiary),400 scrap (bandit camp),,,official=https://wiki.facepunch.com/rust/item/rifle.m39 ; community=https://rustwiki.net/items/m39-rifle ; fandom=https://rust.fandom.com/wiki/M39_Rifle ; conflicts: official lists weapon as not craftable and not researchable ; price 400 scrap from fandom ; range not documented +lr300assaultrifle,LR-300 Assault Rifle,40,x2,500 RPM,30,4 sec,188 m,aim cone 0.2 deg / accuracy 70%,medium,5.56 Rifle Ammo (also HV + explosive + incendiary),,,,official=https://wiki.facepunch.com/rust/item/rifle.lr300 ; community=https://rustwiki.net/items/lr-300-assault-rifle ; fandom=https://rust.fandom.com/wiki/LR-300_Assault_Rifle ; conflicts: recoil community medium vs fandom low ; official lists weapon as not craftable and not researchable ; range 188 m from fandom only +m249,M249,65,x2,500 RPM,100,7.5 sec,Moderate,aim cone 0.2 deg / accuracy 50%,very high,5.56 Rifle Ammo (also HV + explosive + incendiary),,,,official=https://wiki.facepunch.com/rust/item/lmg.m249 ; community=https://rustwiki.net/items/m249 ; fandom=https://rust.fandom.com/wiki/M249 ; conflicts: official lists weapon as not craftable and not researchable ; range Moderate from fandom only +sks,SKS,42.5,x2,400 RPM,15,4.4 sec,,aim cone 0.25 deg,,5.56 Rifle Ammo (also HV + explosive + incendiary),,Workbench Level 3,Research Table 60 scrap ; Tech Tree 500 scrap (bench level 3),official=https://wiki.facepunch.com/rust/item/rifle.sks ; community=https://rustwiki.net/items/sks ; fandom has no page for this weapon ; conflicts: recoil and range not documented +rocketlauncher,Rocket Launcher,0,x1,10 RPM,1,6.162 sec,~250 m,aim cone 2.25 deg / accuracy 90%,very high,Rocket (also HV + incendiary + smoke),,Workbench Level 2,Research Table 120 scrap ; Tech Tree 250 scrap (bench level 2),official=https://wiki.facepunch.com/rust/item/rocket.launcher ; community=https://rustwiki.net/items/rocket-launcher ; fandom=https://rust.fandom.com/wiki/Rocket_Launcher ; conflicts: damage official 0 (rockets do no direct hit damage) vs community 350 (explosion damage) ; blueprint fandom says 500 scrap research vs official 120 + 250 ; range ~250 m from fandom only diff --git a/tools/find-decryptor.py b/tools/find-decryptor.py new file mode 100644 index 0000000..c736c40 --- /dev/null +++ b/tools/find-decryptor.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Find who reads the encrypted classes of the remake. + +The class files inside game.jar start with DE AD C0 DE and are not readable +after that. Something has to decrypt them at runtime, and that something +lives in the launcher, in one of the jars or in the java directory. This +script looks for the marker and for the words that go with it. + +Usage: find-decryptor.py [...] +""" + +import sys +from pathlib import Path + +MARKER = b"\xde\xad\xc0\xde" +WORDS = [b"deadcode", b"DEADC0DE", b"decrypt", b"Decrypt", b"cipher", b"Cipher", + b"ClassLoader", b"defineClass", b"xload", b"scramble", b"obfus"] + + +def scan(path): + hits = [] + try: + data = path.read_bytes() + except OSError: + return hits + pos = data.find(MARKER) + if pos >= 0: + hits.append(("marker", pos)) + for word in WORDS: + pos = data.find(word) + if pos >= 0: + hits.append((word.decode("ascii", "replace"), pos)) + return hits + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 1 + for name in argv[1:]: + path = Path(name) + if path.is_dir(): + for child in path.rglob("*"): + if child.is_file() and child.stat().st_size < 200_000_000: + hits = scan(child) + for what, where in hits: + print("%-70s %s at %d" % (str(child)[-70:], what, where)) + elif path.is_file(): + for what, where in scan(path): + print("%-70s %s at %d" % (str(path)[-70:], what, where)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/inspect-constants.py b/tools/inspect-constants.py new file mode 100644 index 0000000..939c0c4 --- /dev/null +++ b/tools/inspect-constants.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""Look at the constant pool of a class of the remake and see what is hidden. + +The remake obfuscates the string constants: they are not readable in the class +file. This script pulls the UTF-8 entries out of the constant pool and tries +to work out how they were scrambled, because the same cipher is probably what +protects the .obj and .rrs files. + +Usage: inspect-constants.py [...] +""" + +import struct +import sys +from pathlib import Path + +TAGS = { + 1: "Utf8", 3: "Integer", 4: "Float", 5: "Long", 6: "Double", + 7: "Class", 8: "String", 9: "Fieldref", 10: "Methodref", 11: "InterfaceMethodref", + 12: "NameAndType", 15: "MethodHandle", 16: "MethodType", 17: "Dynamic", + 18: "InvokeDynamic", 19: "Module", 20: "Package", +} + + +def read_pool(data): + """The constant pool of a class: a list of (tag, value) pairs.""" + if data[:4] != b"\xca\xfe\xba\xbe": + raise ValueError("not a class file") + count = struct.unpack_from(">H", data, 8)[0] + pool = [] + pos = 10 + i = 1 + while i < count: + tag = data[pos] + pos += 1 + if tag == 1: + length = struct.unpack_from(">H", data, pos)[0] + pos += 2 + pool.append((tag, data[pos:pos + length])) + pos += length + elif tag in (3, 4): + pool.append((tag, data[pos:pos + 4])) + pos += 4 + elif tag in (5, 6): + pool.append((tag, data[pos:pos + 8])) + pos += 8 + elif tag in (7, 8, 16, 19, 20): + pool.append((tag, struct.unpack_from(">H", data, pos)[0])) + pos += 2 + elif tag in (9, 10, 11, 12, 17, 18): + pool.append((tag, struct.unpack_from(">HH", data, pos))) + pos += 4 + elif tag == 15: + pool.append((tag, data[pos:pos + 3])) + pos += 3 + else: + raise ValueError("unknown tag %d at %d" % (tag, pos)) + if tag in (5, 6): + i += 2 + else: + i += 1 + return pool, pos + + +def printable(data): + good = 0 + for b in data: + if 32 <= b < 127: + good += 1 + return good / max(1, len(data)) + + +def crack(text): + """The single byte key that makes a string readable, or None.""" + best = None + for key in range(1, 256): + out = bytes(b ^ key for b in text) + score = printable(out) + if score > 0.98 and all(32 <= b < 127 or b in (9, 10, 13) for b in out): + best = (key, out) + break + return best + + +def main(argv): + if len(argv) < 2: + print(__doc__) + return 1 + for name in argv[1:]: + path = Path(name) + data = path.read_bytes() + pool, _ = read_pool(data) + strings = [v for t, v in pool if t == 1] + readable = [s for s in strings if printable(s) > 0.95] + hidden = [s for s in strings if printable(s) <= 0.95] + print("=== %s: %d strings, %d readable, %d hidden" + % (path.name, len(strings), len(readable), len(hidden))) + for s in readable[:12]: + print(" plain %r" % s[:60]) + for s in hidden[:8]: + cracked = crack(s) + if cracked: + print(" xor %r key 0x%02X" % (s[:40], cracked[0])) + else: + print(" hidden %r" % s[:40]) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv))