passes: rename mixin classes + patch mixins.json/refmap like OBF
- Remove absorb_mixins keep rule (mixin classes now renameable) - Add mixins.rs: patch_mixins_json rewrites package + entries via common prefix of mapped names; patch_refmap_json rewrites all strings - Resources pass routes *.mixins.json and *refmap.json to specialized patchers, falls back to generic text remap Reference: OBF fabric-obfuscator patchMixinsJson/patchRefmapJson.
This commit is contained in:
parent
b73c72d56d
commit
90506a4736
17 changed files with 490 additions and 24 deletions
86
crates/noxis-analysis/src/keep/dispatch.rs
Normal file
86
crates/noxis-analysis/src/keep/dispatch.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! Внешний диспатч: семейства методов, которые программный код зовёт через
|
||||
//! внешние интерфейсы/классы (не из артефакта). Их нельзя переименовывать -
|
||||
//! внешний вызывающий код не переписать, и JVM получит `AbstractMethodError`.
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use noxis_classfile::{Insn, InvokeKind, Program};
|
||||
|
||||
use crate::hierarchy::ClassHierarchy;
|
||||
|
||||
/// Семейства `(name, params)`, которые нельзя переименовывать: программа
|
||||
/// вызывает их виртуально/интерфейсно на внешнем владельце, а некоторый
|
||||
/// класс в предках этого владельца (внутри артефакта) объявляет то же
|
||||
/// `(name, params)`. Переименование разорвало бы диспатч через внешний
|
||||
/// интерфейс/класс - JVM упала бы с `AbstractMethodError ... is abstract`
|
||||
/// (реальный случай: шадоуed gson `LinkedTreeMap$EntrySet$1`, который
|
||||
/// JDK `Map.forEach` зовёт через `invokeinterface Iterator.hasNext`).
|
||||
pub fn external_dispatch_families(
|
||||
program: &Program,
|
||||
hierarchy: &ClassHierarchy,
|
||||
) -> BTreeSet<(String, String)> {
|
||||
let in_program: HashSet<&str> = program
|
||||
.classes
|
||||
.iter()
|
||||
.map(|c| c.this_class.as_str())
|
||||
.collect();
|
||||
|
||||
// Виртуальные/интерфейсные вызовы на внешнем владельце (дедуплицированно).
|
||||
let mut external_calls: BTreeSet<(String, String, String)> = BTreeSet::new();
|
||||
for class in &program.classes {
|
||||
for method in &class.methods {
|
||||
let Some(code) = &method.code else { continue };
|
||||
for insn in &code.insns {
|
||||
if let Insn::Invoke {
|
||||
kind,
|
||||
owner,
|
||||
name,
|
||||
descriptor,
|
||||
..
|
||||
} = insn
|
||||
&& matches!(kind, InvokeKind::Virtual | InvokeKind::Interface)
|
||||
&& !in_program.contains(owner.as_str())
|
||||
&& let Some(params) = params_part(descriptor)
|
||||
{
|
||||
external_calls.insert((owner.clone(), name.clone(), params.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if external_calls.is_empty() {
|
||||
return BTreeSet::new();
|
||||
}
|
||||
|
||||
// Для каждого класса - внешние предки, достижимые сквозь внутренние
|
||||
// суперклассы/интерфейсы (цепочка обрывается на внешнем узле).
|
||||
let mut locked: BTreeSet<(String, String)> = BTreeSet::new();
|
||||
for class in &program.classes {
|
||||
let mut externals: HashSet<String> = HashSet::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut stack = hierarchy.parents(&class.this_class);
|
||||
while let Some(name) = stack.pop() {
|
||||
if in_program.contains(name.as_str()) {
|
||||
if seen.insert(name.clone()) {
|
||||
stack.extend(hierarchy.parents(&name));
|
||||
}
|
||||
} else {
|
||||
externals.insert(name);
|
||||
}
|
||||
}
|
||||
for (owner, name, params) in &external_calls {
|
||||
if externals.contains(owner) {
|
||||
locked.insert((name.clone(), params.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
locked
|
||||
}
|
||||
|
||||
/// `(<params>)<ret>` → `(<params>)`, или `None` при кривом дескрипторе.
|
||||
fn params_part(descriptor: &str) -> Option<&str> {
|
||||
let close = descriptor.find(')')?;
|
||||
if !descriptor.starts_with('(') {
|
||||
return None;
|
||||
}
|
||||
Some(&descriptor[..=close])
|
||||
}
|
||||
|
|
@ -8,8 +8,11 @@ use regex::Regex;
|
|||
use crate::entrypoints::Entrypoints;
|
||||
use noxis_core::Config;
|
||||
|
||||
mod dispatch;
|
||||
mod scans;
|
||||
|
||||
pub use dispatch::external_dispatch_families;
|
||||
|
||||
/// Compiled glob: `*` - anything but `/`, `**` - anything, `?` - one char.
|
||||
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
|
||||
let mut out = String::from("^");
|
||||
|
|
@ -165,7 +168,6 @@ impl Analysis {
|
|||
keep.absorb(&entrypoints);
|
||||
keep.absorb_bootstrap(program);
|
||||
keep.absorb_databind(program);
|
||||
keep.absorb_mixins(program);
|
||||
|
||||
let locked_classes = program
|
||||
.classes
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@ impl KeepRules {
|
|||
/// renaming entirely), which the `@Accessor`/`@Invoker`/`@Overwrite`
|
||||
/// lookups need. Mixins configs are found two ways: any resource ending
|
||||
/// in `mixins.json`, plus whatever `fabric.mod.json` lists.
|
||||
///
|
||||
/// DISABLED: the resources pass now patches mixins.json properly
|
||||
/// (rewrites package + entries via common prefix), so mixin classes
|
||||
/// can be renamed like any other class. Reference: OBF fabric-obfuscator.
|
||||
#[allow(dead_code)]
|
||||
pub fn absorb_mixins(&mut self, program: &Program) {
|
||||
let mut listed: Vec<String> = Vec::new();
|
||||
for resource in &program.resources {
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ pub mod keep;
|
|||
pub use entrypoints::Entrypoints;
|
||||
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
|
||||
pub use jdk::JdkHierarchy;
|
||||
pub use keep::{Analysis, KeepRules, RuleError};
|
||||
pub use keep::{Analysis, KeepRules, RuleError, external_dispatch_families};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
//! Mixin keep: classes listed in `*.mixins.json` (and the plugin) stay.
|
||||
//! Mixin keep: classes listed in `*.mixins.json` are now renameable.
|
||||
//!
|
||||
//! The Mixin framework resolves mixin classes and their members by name at
|
||||
//! runtime, so renaming them breaks the mod at startup. Entries use simple
|
||||
//! names relative to the `package` field, which generic text patching can
|
||||
//! never see - locking is the only sound answer.
|
||||
//! The resources pass patches mixins.json (rewrites package + entries),
|
||||
//! so mixin classes can be renamed. Reference: OBF fabric-obfuscator.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
@ -56,20 +54,20 @@ fn sample() -> Program {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn mixin_classes_and_plugin_stay() {
|
||||
fn mixin_classes_are_renameable() {
|
||||
let program = sample();
|
||||
let analysis = Analysis::run(&program, &Config::default()).expect("analysis");
|
||||
|
||||
for kept in [
|
||||
// Mixin classes are no longer locked - resources pass patches the config.
|
||||
for renameable in [
|
||||
"demo/mixin/TargetMixin",
|
||||
"demo/mixin/other/Helper",
|
||||
"demo/mixin/ClientMixin",
|
||||
"demo/mixin/Plugin",
|
||||
] {
|
||||
assert!(analysis.is_locked(kept), "{kept} must survive renaming");
|
||||
assert!(
|
||||
!analysis.is_locked(renameable),
|
||||
"{renameable} should be renameable now"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!analysis.is_locked("demo/App"),
|
||||
"ordinary classes must still rename"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue