Proven by paranoid runs over lorep (17 classes, fully green), LoVisual (4081 classes) and oc2r (1278 classes) with per-class java -Xverify:all, each fix first reproduced as a red test: - merge: Top absorbs instead of Top-wins (javac declares Top for slots unwritten on some inflow; precise types reject those edges). Regression: loop_head_keeps_top_for_loop_local. - emit: collapse [Long|Double, Top] pairs on operand stacks (javac writes one entry; the pair is rejected). Regression: wide_stack_join_emits_single_entries. - xfer ArrayLoad: push the wide pair, fixing lcmp underflow. Regression: array_load_wide_pushes_pair. - xfer <init>: convert only the receiver's own allocation site instead of every Uninitialized (nested construction clobbered the outer object). Regression: nested_construction_keeps_outer. - xfer ANewArray: array operands gain one dimension ([B] -> [[B) instead of an illegal L-wrap ([L[B;). Covered by the same test. - jar load: duplicate class names (multi-release module-info twins) keep the first occurrence instead of crashing save. Test: duplicate_class_names_keep_first. - strings/constants: skip methods past MAX_METHOD_INSNS (8000) so giants cannot overflow the 64KB writer ceiling. Tests: oversized_methods_are_skipped in both passes. - merge errors now name both types being merged. Also: em-dash removed repo-wide (724 spots, rule added to todo.md), allow-lints only in tests (scratch example deleted), code comments migrated to English in every file this change touches.
112 lines
4.1 KiB
Rust
112 lines
4.1 KiB
Rust
//! Обратная таблица маппингов: деобфускация имён и stack-trace translate.
|
||
|
||
use std::collections::BTreeMap;
|
||
|
||
use serde::Serialize;
|
||
|
||
/// Ключ метода: `owner/name+descriptor`.
|
||
pub fn method_key(owner: &str, name: &str, descriptor: &str) -> String {
|
||
format!("{owner}/{name}{descriptor}")
|
||
}
|
||
|
||
/// Ключ поля: `owner/name:descriptor`.
|
||
pub fn field_key(owner: &str, name: &str, descriptor: &str) -> String {
|
||
format!("{owner}/{name}:{descriptor}")
|
||
}
|
||
|
||
/// Таблицы «оригинал → обфусцированное».
|
||
///
|
||
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
|
||
/// (аналог ZKM Stack Trace Translate), (2) аудит - что именно поменялось.
|
||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||
pub struct Mapping {
|
||
/// internal name класса → новый internal name
|
||
pub classes: BTreeMap<String, String>,
|
||
/// `owner/name+desc` → новое имя метода (владелец остаётся прежним по ключу)
|
||
pub methods: BTreeMap<String, String>,
|
||
/// `owner/name:desc` → новое имя поля
|
||
pub fields: BTreeMap<String, String>,
|
||
/// имя локальной переменной/параметра в методе → новое (по номеру слота)
|
||
pub locals: BTreeMap<String, String>,
|
||
/// оригинальный путь ресурса → новый
|
||
pub resources: BTreeMap<String, String>,
|
||
}
|
||
|
||
impl Mapping {
|
||
/// Новое имя класса, если он переименован.
|
||
pub fn class(&self, from: &str) -> Option<&str> {
|
||
self.classes.get(from).map(String::as_str)
|
||
}
|
||
|
||
/// Новое имя метода.
|
||
pub fn method(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
|
||
self.methods
|
||
.get(&method_key(owner, name, descriptor))
|
||
.map(String::as_str)
|
||
}
|
||
|
||
/// Новое имя поля.
|
||
pub fn field(&self, owner: &str, name: &str, descriptor: &str) -> Option<&str> {
|
||
self.fields
|
||
.get(&field_key(owner, name, descriptor))
|
||
.map(String::as_str)
|
||
}
|
||
|
||
/// Число переименований всех видов.
|
||
pub fn len(&self) -> usize {
|
||
self.classes.len() + self.methods.len() + self.fields.len() + self.locals.len()
|
||
}
|
||
|
||
/// Пустая ли таблица.
|
||
pub fn is_empty(&self) -> bool {
|
||
self.len() == 0
|
||
}
|
||
|
||
/// Человекочитаемая таблица для файла маппингов и deobfuscator'а.
|
||
pub fn to_text(&self) -> String {
|
||
let mut out = String::new();
|
||
out.push_str("# classes\n");
|
||
for (from, to) in &self.classes {
|
||
out.push_str(&format!("{from} -> {to}\n"));
|
||
}
|
||
out.push_str("\n# methods\n");
|
||
for (from, to) in &self.methods {
|
||
out.push_str(&format!("{from} -> {to}\n"));
|
||
}
|
||
out.push_str("\n# fields\n");
|
||
for (from, to) in &self.fields {
|
||
out.push_str(&format!("{from} -> {to}\n"));
|
||
}
|
||
out.push_str("\n# resources\n");
|
||
for (from, to) in &self.resources {
|
||
out.push_str(&format!("{from} -> {to}\n"));
|
||
}
|
||
out
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn roundtrip_lookup() {
|
||
let mut m = Mapping::default();
|
||
m.classes.insert("com/example/Main".into(), "a/B".into());
|
||
m.methods
|
||
.insert(method_key("a/B", "run", "()V"), "x".into());
|
||
assert_eq!(m.class("com/example/Main"), Some("a/B"));
|
||
assert_eq!(m.method("a/B", "run", "()V"), Some("x"));
|
||
assert!(m.method("a/B", "other", "()V").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn text_report_contains_sections() {
|
||
let mut m = Mapping::default();
|
||
m.classes.insert("A".into(), "B".into());
|
||
let text = m.to_text();
|
||
assert!(text.contains("# classes"));
|
||
assert!(text.contains("A -> B"));
|
||
}
|
||
}
|