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
|
|
@ -11,6 +11,7 @@ noxis-analysis = { workspace = true }
|
|||
noxis-classfile = { workspace = true }
|
||||
noxis-core = { workspace = true }
|
||||
noxis-runtime = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use noxis_analysis::{Analysis, ClassHierarchy};
|
||||
use noxis_analysis::{Analysis, ClassHierarchy, external_dispatch_families};
|
||||
use noxis_classfile::Program;
|
||||
use noxis_core::{Config, Context, NameStyle, Rng};
|
||||
|
||||
|
|
@ -97,6 +97,7 @@ impl RenameMaps {
|
|||
pub(super) fn build_virtual_families(
|
||||
program: &Program,
|
||||
analysis: &Analysis,
|
||||
hierarchy: &ClassHierarchy,
|
||||
) -> (BTreeMap<FamilyKey, Vec<MemberKey>>, BTreeSet<FamilyKey>) {
|
||||
let mut families: BTreeMap<FamilyKey, Vec<MemberKey>> = BTreeMap::new();
|
||||
let mut locked: BTreeSet<FamilyKey> = BTreeSet::new();
|
||||
|
|
@ -124,6 +125,11 @@ pub(super) fn build_virtual_families(
|
|||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Семейства, которые диспатчатся через внешние интерфейсы/классы, обязаны
|
||||
// сохранить имя: внешний вызывающий код не переименовать.
|
||||
locked.extend(external_dispatch_families(program, hierarchy));
|
||||
|
||||
(families, locked)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ impl Pass for RenamePass {
|
|||
// rename splits the override chain (the JVM then fails dispatch
|
||||
// with AbstractMethodError, real case: shaded gson's package-private
|
||||
// `ReflectionHelper$RecordHelper.isRecord` chain).
|
||||
let (families, locked_families) = build_virtual_families(program, &analysis);
|
||||
let (families, locked_families) = build_virtual_families(program, &analysis, &hierarchy);
|
||||
|
||||
// 1. Class names.
|
||||
{
|
||||
|
|
|
|||
153
crates/noxis-passes/src/resources/mixins.rs
Normal file
153
crates/noxis-passes/src/resources/mixins.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
//! Fabric mixins.json and refmap: rewrite package + all references.
|
||||
//!
|
||||
//! Mixins reference classes by short names relative to `package`.
|
||||
//! Plain full-name text replacement does not work. Algorithm:
|
||||
//! 1. Collect full paths of all mixin entries (package + short name).
|
||||
//! 2. Find common prefix of mapped names.
|
||||
//! 3. Rewrite `package` to that prefix, entries relative to it.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Patch mixins.json: package, entries in mixins/client/server, plugin.
|
||||
pub fn patch_mixins_json(text: &str, map: &BTreeMap<String, String>) -> Option<String> {
|
||||
let mut root: serde_json::Value = serde_json::from_str(text).ok()?;
|
||||
|
||||
let pkg_dot = root.get("package").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let pkg_path = pkg_dot.replace('.', "/");
|
||||
|
||||
let sections = ["mixins", "client", "server"];
|
||||
|
||||
// 1. Collect full paths of all entries.
|
||||
let mut full_names: Vec<String> = Vec::new();
|
||||
for section in sections {
|
||||
let Some(arr) = root.get(section).and_then(|v| v.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
for el in arr {
|
||||
let Some(short) = el.as_str() else { continue };
|
||||
let full = if pkg_path.is_empty() {
|
||||
short.replace('.', "/")
|
||||
} else {
|
||||
format!("{}/{}", pkg_path, short.replace('.', "/"))
|
||||
};
|
||||
let mapped = map.get(&full).cloned().unwrap_or(full);
|
||||
full_names.push(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Common prefix of mapped names.
|
||||
let common = common_prefix(&full_names);
|
||||
let common_dot = common.replace('/', ".");
|
||||
|
||||
// 3. Rewrite package and entries.
|
||||
for section in sections {
|
||||
let Some(arr) = root.get(section).and_then(|v| v.as_array()) else {
|
||||
continue;
|
||||
};
|
||||
let entries: Vec<String> = arr
|
||||
.iter()
|
||||
.filter_map(|el| el.as_str())
|
||||
.map(|short| {
|
||||
let full = if pkg_path.is_empty() {
|
||||
short.replace('.', "/")
|
||||
} else {
|
||||
format!("{}/{}", pkg_path, short.replace('.', "/"))
|
||||
};
|
||||
let mapped = map.get(&full).cloned().unwrap_or(full);
|
||||
let tail = if !common.is_empty() && mapped.starts_with(&format!("{common}/")) {
|
||||
&mapped[common.len() + 1..]
|
||||
} else {
|
||||
&mapped
|
||||
};
|
||||
tail.replace('/', ".")
|
||||
})
|
||||
.collect();
|
||||
root[section] =
|
||||
serde_json::Value::Array(entries.into_iter().map(serde_json::Value::String).collect());
|
||||
}
|
||||
root["package"] = serde_json::Value::String(common_dot);
|
||||
|
||||
// 4. Plugin (full class name).
|
||||
if let Some(plugin) = root.get("plugin").and_then(|v| v.as_str()) {
|
||||
let cls = plugin.replace('.', "/");
|
||||
if let Some(mapped) = map.get(&cls) {
|
||||
root["plugin"] = serde_json::Value::String(mapped.replace('/', "."));
|
||||
}
|
||||
}
|
||||
|
||||
Some(serde_json::to_string_pretty(&root).unwrap_or_else(|_| text.to_string()))
|
||||
}
|
||||
|
||||
/// Patch refmap.json: recursively replace all string values.
|
||||
pub fn patch_refmap_json(text: &str, map: &BTreeMap<String, String>) -> Option<String> {
|
||||
let mut root: serde_json::Value = serde_json::from_str(text).ok()?;
|
||||
remap_json_value(&mut root, map);
|
||||
Some(serde_json::to_string_pretty(&root).unwrap_or_else(|_| text.to_string()))
|
||||
}
|
||||
|
||||
/// Recursively replace strings in JSON via mapping.
|
||||
fn remap_json_value(value: &mut serde_json::Value, map: &BTreeMap<String, String>) {
|
||||
match value {
|
||||
serde_json::Value::Object(obj) => {
|
||||
let remapped: serde_json::Map<String, serde_json::Value> = obj
|
||||
.iter()
|
||||
.map(|(key, val)| {
|
||||
let new_key = remap_reference(key, map);
|
||||
let mut new_val = val.clone();
|
||||
remap_json_value(&mut new_val, map);
|
||||
(new_key, new_val)
|
||||
})
|
||||
.collect();
|
||||
*obj = remapped;
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for el in arr.iter_mut() {
|
||||
remap_json_value(el, map);
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
*s = remap_reference(s, map);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace class/method/field reference in a string.
|
||||
fn remap_reference(s: &str, map: &BTreeMap<String, String>) -> String {
|
||||
// Internal name (pkg/Class) - return internal form.
|
||||
if let Some(mapped) = map.get(s) {
|
||||
return mapped.clone();
|
||||
}
|
||||
// Dotted name (pkg.Class) - return dotted form.
|
||||
let internal = s.replace('.', "/");
|
||||
if internal != s
|
||||
&& let Some(mapped) = map.get(&internal)
|
||||
{
|
||||
return mapped.replace('/', ".");
|
||||
}
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
/// Find common path prefix of names (separator `/`).
|
||||
fn common_prefix(names: &[String]) -> String {
|
||||
if names.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
let parts: Vec<Vec<&str>> = names.iter().map(|s| s.split('/').collect()).collect();
|
||||
let first = &parts[0];
|
||||
if first.len() <= 1 {
|
||||
return String::new();
|
||||
}
|
||||
let mut common = first.len() - 1;
|
||||
for cur in parts.iter().skip(1) {
|
||||
let mut k = 0;
|
||||
while k < common && k < cur.len() && first[k] == cur[k] {
|
||||
k += 1;
|
||||
}
|
||||
common = k;
|
||||
}
|
||||
if common == 0 {
|
||||
return String::new();
|
||||
}
|
||||
first[..common].join("/")
|
||||
}
|
||||
|
|
@ -14,12 +14,16 @@
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod mixins;
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||
|
||||
pub use mixins::{patch_mixins_json, patch_refmap_json};
|
||||
|
||||
/// Расширения текстовых ресурсов, которые имеют смысл переписывать.
|
||||
const TEXT_EXTS: [&str; 9] = [
|
||||
".json",
|
||||
|
|
@ -68,7 +72,14 @@ impl Pass for ResourcesPass {
|
|||
skipped += 1;
|
||||
continue;
|
||||
};
|
||||
let updated = remap_text(text, &map);
|
||||
let lower = resource.path.to_ascii_lowercase();
|
||||
let updated = if lower.ends_with(".mixins.json") || lower.ends_with("mixins.json") {
|
||||
patch_mixins_json(text, &map).unwrap_or_else(|| remap_text(text, &map))
|
||||
} else if lower.ends_with("refmap.json") {
|
||||
patch_refmap_json(text, &map).unwrap_or_else(|| remap_text(text, &map))
|
||||
} else {
|
||||
remap_text(text, &map)
|
||||
};
|
||||
if updated == text {
|
||||
skipped += 1;
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
use noxis_classfile::{Program, Resource};
|
||||
use noxis_core::{Config, Context, Pass};
|
||||
|
||||
use super::{ResourcesPass, is_text, remap_text};
|
||||
use super::{ResourcesPass, is_text, patch_mixins_json, patch_refmap_json, remap_text};
|
||||
|
||||
fn mapping() -> std::collections::BTreeMap<String, String> {
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
|
|
@ -158,3 +158,78 @@ fn pass_is_a_noop_without_a_mapping() {
|
|||
"должна быть заметка о пустом маппинге"
|
||||
);
|
||||
}
|
||||
|
||||
fn mixin_mapping() -> std::collections::BTreeMap<String, String> {
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("dev/loki/mixins/MixinA".into(), "xYz/abc/MixinA".into());
|
||||
map.insert("dev/loki/mixins/MixinB".into(), "xYz/abc/MixinB".into());
|
||||
map.insert("dev/loki/mixins/Plugin".into(), "xYz/abc/Plugin".into());
|
||||
map
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_mixins_json_rewrites_package_and_entries() {
|
||||
let json = r#"{
|
||||
"package": "dev.loki.mixins",
|
||||
"plugin": "dev.loki.mixins.Plugin",
|
||||
"mixins": ["MixinA", "MixinB"]
|
||||
}"#;
|
||||
let patched = patch_mixins_json(json, &mixin_mapping()).expect("patch");
|
||||
assert!(
|
||||
patched.contains("\"package\": \"xYz.abc\""),
|
||||
"got: {patched}"
|
||||
);
|
||||
assert!(patched.contains("\"MixinA\""), "got: {patched}");
|
||||
assert!(patched.contains("\"xYz.abc.Plugin\""), "got: {patched}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_mixins_json_handles_empty_package() {
|
||||
let json = r#"{"mixins": ["a.b.MixinA"]}"#;
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("a/b/MixinA".into(), "x/M".into());
|
||||
let patched = patch_mixins_json(json, &map).expect("patch");
|
||||
assert!(patched.contains("\"M\""), "got: {patched}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_refmap_json_rewrites_strings() {
|
||||
let json = r#"{
|
||||
"mappings": {
|
||||
"dev/loki/mixins/MixinA": {"method_1234": "a/b/Target"}
|
||||
},
|
||||
"data": {"dev/loki/mixins/MixinA": {"method_1234": "field_5678"}}
|
||||
}"#;
|
||||
let mut map = std::collections::BTreeMap::new();
|
||||
map.insert("a/b/Target".into(), "c/D".into());
|
||||
let patched = patch_refmap_json(json, &map).expect("patch");
|
||||
assert!(patched.contains("c/D"), "got: {patched}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_patches_mixins_json_via_specialized_patcher() {
|
||||
let mut program = Program {
|
||||
classes: Vec::new(),
|
||||
resources: vec![Resource {
|
||||
path: "lovisual.mixins.json".into(),
|
||||
data: br#"{
|
||||
"package": "dev.loki.mixins",
|
||||
"mixins": ["MixinA"]
|
||||
}"#
|
||||
.to_vec(),
|
||||
}],
|
||||
};
|
||||
|
||||
let mut ctx = Context::new(Config::default());
|
||||
ctx.mapping
|
||||
.classes
|
||||
.insert("dev/loki/mixins/MixinA".into(), "xYz/abc/MixinA".into());
|
||||
|
||||
let report = ResourcesPass
|
||||
.apply(&mut ctx, &mut program)
|
||||
.expect("resources");
|
||||
|
||||
assert_eq!(report.transformed, 1);
|
||||
let text = String::from_utf8(program.resources[0].data.clone()).unwrap();
|
||||
assert!(text.contains("\"package\": \"xYz.abc\""), "got: {text}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,3 +103,59 @@ fn package_private_overrides_stay_consistent_without_public_rename() {
|
|||
"families off must keep the chain untouched, got {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `demo/Iter` implements the external `java/util/Iterator`; `demo/Use` calls
|
||||
/// `invokeinterface Iterator.hasNext`. The `(hasNext, ()Z)` family must keep
|
||||
/// its original name: renaming it splits dispatch through the JDK interface
|
||||
/// (the JVM then fails with `AbstractMethodError ... is abstract`, real case:
|
||||
/// shaded gson's `LinkedTreeMap$EntrySet$1` iterator reached from
|
||||
/// `Map.forEach`).
|
||||
fn external_iterator_sample() -> Program {
|
||||
let iter = ClassBuilder::new("demo/Iter", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.interface("java/util/Iterator")
|
||||
.method_with_code(0x0001, "hasNext", "()Z", check_code())
|
||||
.build();
|
||||
let use_code = Code {
|
||||
max_stack: 1,
|
||||
max_locals: 1,
|
||||
insns: vec![
|
||||
Insn::AconstNull,
|
||||
Insn::Invoke {
|
||||
kind: noxis_classfile::InvokeKind::Interface,
|
||||
owner: "java/util/Iterator".to_string(),
|
||||
name: "hasNext".to_string(),
|
||||
descriptor: "()Z".to_string(),
|
||||
itf: true,
|
||||
},
|
||||
Insn::Return(Some(Prim::Int)),
|
||||
],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
let use_class = ClassBuilder::new("demo/Use", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0001, "use", "()Z", use_code)
|
||||
.build();
|
||||
Program {
|
||||
classes: vec![iter, use_class],
|
||||
resources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn externally_dispatched_interface_method_keeps_its_name() {
|
||||
let mut program = external_iterator_sample();
|
||||
run(&mut program, true);
|
||||
|
||||
let kept = program.classes.iter().any(|c| {
|
||||
c.methods
|
||||
.iter()
|
||||
.any(|m| m.name == "hasNext" && m.descriptor == "()Z")
|
||||
});
|
||||
assert!(
|
||||
kept,
|
||||
"hasNext implements java/util/Iterator and must not be renamed"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue