passes: junk classes with real bodies and no fingerprint
NoisePass now builds branching helper methods (counted int loop, random arithmetic, even/odd merge, non-throwing ops only) whose results the entry sums into a private static field, instead of an empty run()V. Names come from the same NameGenerator styles as real code and live in a random real package (no more shadow/decoy uniformity for grep to catch); every generated method gets fresh StackMapTable frames. weave_first_reachable takes (class, entry) pairs. New NoiseConfig.junk_methods_per_class (default 3). Tests assert invariants (unique valid identifiers, calls, field store, determinism) plus real-JVM verification of junk classes; paranoid+junk_ratio=1 smoke on lorep: 18 junk classes, all load.
This commit is contained in:
parent
74ad4552e4
commit
2cbb9db31e
11 changed files with 648 additions and 159 deletions
|
|
@ -97,7 +97,7 @@ fn build_frame(
|
||||||
})?;
|
})?;
|
||||||
if !(1..=3).contains(&count) {
|
if !(1..=3).contains(&count) {
|
||||||
return Err(ClassfileError::Write(format!(
|
return Err(ClassfileError::Write(format!(
|
||||||
"append-кадр на {count} local'ов (нужно 1–3)"
|
"append-кадр на {count} local'ов (нужно 1-3)"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
RtFrame::AppendFrame {
|
RtFrame::AppendFrame {
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,9 @@ impl Default for ConstantsConfig {
|
||||||
pub struct NoiseConfig {
|
pub struct NoiseConfig {
|
||||||
/// How many junk classes per real one (0 = disabled).
|
/// How many junk classes per real one (0 = disabled).
|
||||||
pub junk_ratio: usize,
|
pub junk_ratio: usize,
|
||||||
|
/// Branching helper methods per junk class (their results feed a
|
||||||
|
/// static field so the bodies read as live code, not dead weight).
|
||||||
|
pub junk_methods_per_class: usize,
|
||||||
/// Number of junk annotations per class.
|
/// Number of junk annotations per class.
|
||||||
pub junk_annotations: usize,
|
pub junk_annotations: usize,
|
||||||
/// Weave junk classes into the reachability graph (required for them to
|
/// Weave junk classes into the reachability graph (required for them to
|
||||||
|
|
@ -159,6 +162,7 @@ impl Default for NoiseConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
junk_ratio: 0,
|
junk_ratio: 0,
|
||||||
|
junk_methods_per_class: 3,
|
||||||
junk_annotations: 0,
|
junk_annotations: 0,
|
||||||
weave_reachable: true,
|
weave_reachable: true,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ impl Rng {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Перемешать срез на месте (Fisher–Yates).
|
/// Перемешать срез на месте (Fisher-Yates).
|
||||||
pub fn shuffle<T>(&mut self, items: &mut [T]) {
|
pub fn shuffle<T>(&mut self, items: &mut [T]) {
|
||||||
for i in (1..items.len()).rev() {
|
for i in (1..items.len()).rev() {
|
||||||
let j = self.below((i + 1) as u64) as usize;
|
let j = self.below((i + 1) as u64) as usize;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ fn negate_cond(cond: Cond) -> Cond {
|
||||||
|
|
||||||
/// Turn `if-cond T` into `if-not SKIP; goto T; SKIP:` (`butterfly`).
|
/// Turn `if-cond T` into `if-not SKIP; goto T; SKIP:` (`butterfly`).
|
||||||
///
|
///
|
||||||
/// `rate` is a 0–100 percentage applied per branch. Semantics are unchanged:
|
/// `rate` is a 0-100 percentage applied per branch. Semantics are unchanged:
|
||||||
/// the inverted branch skips a `goto` to the original target, so the taken /
|
/// the inverted branch skips a `goto` to the original target, so the taken /
|
||||||
/// not-taken destinations are exactly the old ones. A `nop` pads the fresh
|
/// not-taken destinations are exactly the old ones. A `nop` pads the fresh
|
||||||
/// mark: whatever follows (an original mark, even a handler entry with its
|
/// mark: whatever follows (an original mark, even a handler entry with its
|
||||||
|
|
|
||||||
259
crates/noxis-passes/src/noise/body.rs
Normal file
259
crates/noxis-passes/src/noise/body.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
//! Junk method bodies: plausible branching integer code.
|
||||||
|
//!
|
||||||
|
//! A helper is `private static int helper()`: two int locals (`acc`, `i`),
|
||||||
|
//! a counted loop with a few random binary ops plus one even/odd if/else
|
||||||
|
//! merge for a real branching shape. Everything is stack-neutral by
|
||||||
|
//! construction and uses only non-throwing integer ops: woven junk really
|
||||||
|
//! runs, so it must never throw.
|
||||||
|
//!
|
||||||
|
//! Labels start at fixed small numbers: bodies are built from scratch, so
|
||||||
|
//! nothing collides (labels are per-method).
|
||||||
|
|
||||||
|
use noxis_classfile::{Arith, BranchCond, Code, Cond, Const, Insn, Prim, SlotKind};
|
||||||
|
use noxis_core::Rng;
|
||||||
|
|
||||||
|
/// Non-throwing integer ops for junk arithmetic (`Div`/`Rem` could trap).
|
||||||
|
const OPS: [Arith; 8] = [
|
||||||
|
Arith::Add,
|
||||||
|
Arith::Sub,
|
||||||
|
Arith::Mul,
|
||||||
|
Arith::And,
|
||||||
|
Arith::Or,
|
||||||
|
Arith::Xor,
|
||||||
|
Arith::Shl,
|
||||||
|
Arith::Shr,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Fresh-label block for one helper (head, end, odd, merge).
|
||||||
|
const HEAD: u32 = 1;
|
||||||
|
const END: u32 = 2;
|
||||||
|
const ODD: u32 = 3;
|
||||||
|
const MERGE: u32 = 4;
|
||||||
|
|
||||||
|
/// Build one helper body.
|
||||||
|
pub(super) fn helper_body(rng: &mut Rng) -> Code {
|
||||||
|
let bound = 4 + rng.below(29) as i32;
|
||||||
|
let mut insns = vec![
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::Store {
|
||||||
|
slot: 1,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Mark(HEAD),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 1,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(Const::Int(bound)),
|
||||||
|
Insn::Branch(BranchCond::Int(Cond::Ge), END),
|
||||||
|
];
|
||||||
|
// Straight-line accumulator churn: `acc = acc op i`, two to four ops.
|
||||||
|
for _ in 0..2 + rng.below(3) {
|
||||||
|
let op = OPS[rng.below(OPS.len() as u64) as usize];
|
||||||
|
insns.extend([
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Load {
|
||||||
|
slot: 1,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Binary {
|
||||||
|
prim: Prim::Int,
|
||||||
|
op,
|
||||||
|
},
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
// Even/odd merge: `if ((i & 1) == 0) acc += c1 else acc ^= c2`.
|
||||||
|
let (even, odd) = (rng.next_u64() as i32, rng.next_u64() as i32);
|
||||||
|
insns.extend([
|
||||||
|
Insn::Load {
|
||||||
|
slot: 1,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(Const::Int(1)),
|
||||||
|
Insn::Binary {
|
||||||
|
prim: Prim::Int,
|
||||||
|
op: Arith::And,
|
||||||
|
},
|
||||||
|
Insn::Branch(BranchCond::Zero(Cond::Ne), ODD),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(Const::Int(even)),
|
||||||
|
Insn::Binary {
|
||||||
|
prim: Prim::Int,
|
||||||
|
op: Arith::Add,
|
||||||
|
},
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Goto(MERGE),
|
||||||
|
Insn::Mark(ODD),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Const(Const::Int(odd)),
|
||||||
|
Insn::Binary {
|
||||||
|
prim: Prim::Int,
|
||||||
|
op: Arith::Xor,
|
||||||
|
},
|
||||||
|
Insn::Store {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Mark(MERGE),
|
||||||
|
]);
|
||||||
|
insns.extend([
|
||||||
|
Insn::Iinc { index: 1, delta: 1 },
|
||||||
|
Insn::Goto(HEAD),
|
||||||
|
Insn::Mark(END),
|
||||||
|
Insn::Load {
|
||||||
|
slot: 0,
|
||||||
|
kind: SlotKind::Int,
|
||||||
|
},
|
||||||
|
Insn::Return(Some(Prim::Int)),
|
||||||
|
]);
|
||||||
|
Code {
|
||||||
|
max_stack: 4,
|
||||||
|
max_locals: 2,
|
||||||
|
insns,
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the entry body: call every helper, sum the results into the static
|
||||||
|
/// field (a bare `void` call with a dropped result is a recognizable junk
|
||||||
|
/// pattern on its own). With no helpers the entry is a bare return.
|
||||||
|
pub(super) fn entry_body(callees: &[(String, String)], owner: &str, field: &str) -> Code {
|
||||||
|
let mut insns = Vec::new();
|
||||||
|
let mut first = true;
|
||||||
|
for (callee_owner, callee_name) in callees {
|
||||||
|
insns.push(Insn::Invoke {
|
||||||
|
kind: noxis_classfile::InvokeKind::Static,
|
||||||
|
owner: callee_owner.clone(),
|
||||||
|
name: callee_name.clone(),
|
||||||
|
descriptor: "()I".to_string(),
|
||||||
|
itf: false,
|
||||||
|
});
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
insns.push(Insn::Binary {
|
||||||
|
prim: Prim::Int,
|
||||||
|
op: Arith::Add,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if callees.is_empty() {
|
||||||
|
insns.push(Insn::Return(None));
|
||||||
|
} else {
|
||||||
|
insns.push(Insn::PutStatic {
|
||||||
|
owner: owner.to_string(),
|
||||||
|
name: field.to_string(),
|
||||||
|
descriptor: "I".to_string(),
|
||||||
|
});
|
||||||
|
insns.push(Insn::Return(None));
|
||||||
|
}
|
||||||
|
Code {
|
||||||
|
max_stack: 2,
|
||||||
|
max_locals: 0,
|
||||||
|
insns,
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Helpers branch and loop instead of returning immediately.
|
||||||
|
#[test]
|
||||||
|
fn helper_has_branches_and_loop() {
|
||||||
|
let mut rng = Rng::from_seed(3);
|
||||||
|
let code = helper_body(&mut rng);
|
||||||
|
let has = |pred: &dyn Fn(&Insn) -> bool| code.insns.iter().any(pred);
|
||||||
|
assert!(has(&|i| matches!(i, Insn::Branch(..))), "no branch");
|
||||||
|
assert!(has(&|i| matches!(i, Insn::Goto(_))), "no loop back-edge");
|
||||||
|
assert!(has(&|i| matches!(i, Insn::Binary { .. })), "no arithmetic");
|
||||||
|
assert!(has(&|i| matches!(i, Insn::Iinc { .. })), "no counter");
|
||||||
|
assert!(
|
||||||
|
has(&|i| matches!(i, Insn::Return(Some(Prim::Int)))),
|
||||||
|
"no int return"
|
||||||
|
);
|
||||||
|
assert_eq!(code.max_locals, 2);
|
||||||
|
// Every jump target is declared.
|
||||||
|
let marks: std::collections::BTreeSet<u32> = code
|
||||||
|
.insns
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| match i {
|
||||||
|
Insn::Mark(label) => Some(*label),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for insn in &code.insns {
|
||||||
|
for target in insn.targets() {
|
||||||
|
assert!(marks.contains(&target), "dangling label {target}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same seed, same body.
|
||||||
|
#[test]
|
||||||
|
fn helper_is_deterministic() {
|
||||||
|
let first = helper_body(&mut Rng::from_seed(9));
|
||||||
|
let second = helper_body(&mut Rng::from_seed(9));
|
||||||
|
assert_eq!(first.insns, second.insns);
|
||||||
|
let third = helper_body(&mut Rng::from_seed(10));
|
||||||
|
assert_ne!(first.insns, third.insns);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The entry calls every helper and stores the sum into the field.
|
||||||
|
#[test]
|
||||||
|
fn entry_calls_helpers_and_stores() {
|
||||||
|
let callees = vec![
|
||||||
|
("junk/A".to_string(), "h0".to_string()),
|
||||||
|
("junk/A".to_string(), "h1".to_string()),
|
||||||
|
];
|
||||||
|
let code = entry_body(&callees, "junk/A", "f");
|
||||||
|
let invokes = code
|
||||||
|
.insns
|
||||||
|
.iter()
|
||||||
|
.filter(|i| matches!(i, Insn::Invoke { .. }))
|
||||||
|
.count();
|
||||||
|
assert_eq!(invokes, 2);
|
||||||
|
assert!(
|
||||||
|
code.insns
|
||||||
|
.iter()
|
||||||
|
.any(|i| matches!(i, Insn::PutStatic { name, .. } if name == "f")),
|
||||||
|
"no field store"
|
||||||
|
);
|
||||||
|
assert!(matches!(code.insns.last(), Some(Insn::Return(None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No helpers, no calls: a bare return, still valid.
|
||||||
|
#[test]
|
||||||
|
fn entry_without_helpers_returns() {
|
||||||
|
let code = entry_body(&[], "junk/A", "f");
|
||||||
|
assert_eq!(code.insns, vec![Insn::Return(None)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,38 @@
|
||||||
//! Пасс шума: junk-классы и вплетание их в граф достижимости.
|
//! Noise pass: junk classes with real bodies, woven into reachability.
|
||||||
//!
|
//!
|
||||||
//! Мёртвый код выкидывается shrink-анализом, поэтому каждый junk-класс
|
//! Dead code gets dropped by shrink analysis, so every junk class gets an
|
||||||
//! получает ребро из живого метода (`invokestatic shadow/decoyN.run()V`).
|
//! edge from live code (an `invokestatic` of its entry). Junk names come
|
||||||
//! Junk-аннотации не генерируются: сырые атрибуты не несут свежие индексы пула.
|
//! from the same generator (and styles) as real code: self-describing
|
||||||
|
//! `shadow/decoy` names with a uniform `run` entry would be a grep pattern
|
||||||
|
//! of their own.
|
||||||
|
|
||||||
|
mod body;
|
||||||
mod weave;
|
mod weave;
|
||||||
|
|
||||||
use noxis_classfile::{ClassBuilder, Program, Version, empty_code};
|
#[cfg(test)]
|
||||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
mod tests;
|
||||||
|
|
||||||
|
use crate::namegen::NameGenerator;
|
||||||
|
use crate::rename::assign::parent_package;
|
||||||
|
use body::{entry_body, helper_body};
|
||||||
|
use noxis_analysis::ClassHierarchy;
|
||||||
|
use noxis_classfile::{ClassBuilder, Field, Program, Version, compute_frames};
|
||||||
|
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||||
use weave::weave_first_reachable;
|
use weave::weave_first_reachable;
|
||||||
|
|
||||||
/// Префикс детерминированных имён junk-классов.
|
/// Flags for the junk entry (`public static`, callable from live code).
|
||||||
const JUNK_PREFIX: &str = "shadow/decoy";
|
|
||||||
|
|
||||||
/// Имя и дескриптор точки входа junk-класса.
|
|
||||||
const JUNK_ENTRY: (&str, &str) = ("run", "()V");
|
|
||||||
|
|
||||||
/// Флаги `public static` для junk-метода.
|
|
||||||
const ACC_PUBLIC_STATIC: u16 = 0x0009;
|
const ACC_PUBLIC_STATIC: u16 = 0x0009;
|
||||||
|
/// Flags for helpers and the accumulator field (`private static`).
|
||||||
|
const ACC_PRIVATE_STATIC: u16 = 0x000A;
|
||||||
|
|
||||||
/// Пасс шума.
|
/// The entry descriptor (always `()V`).
|
||||||
|
const ENTRY_DESCRIPTOR: &str = "()V";
|
||||||
|
/// Helper methods return the accumulator (`int`).
|
||||||
|
const HELPER_DESCRIPTOR: &str = "()I";
|
||||||
|
/// The accumulator field type.
|
||||||
|
const FIELD_DESCRIPTOR: &str = "I";
|
||||||
|
|
||||||
|
/// The noise pass.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct NoisePass;
|
pub struct NoisePass;
|
||||||
|
|
||||||
|
|
@ -31,6 +43,7 @@ impl Pass for NoisePass {
|
||||||
|
|
||||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
|
||||||
let ratio = ctx.config.noise.junk_ratio;
|
let ratio = ctx.config.noise.junk_ratio;
|
||||||
|
let methods_per_class = ctx.config.noise.junk_methods_per_class;
|
||||||
let annotations = ctx.config.noise.junk_annotations;
|
let annotations = ctx.config.noise.junk_annotations;
|
||||||
let weave = ctx.config.noise.weave_reachable;
|
let weave = ctx.config.noise.weave_reachable;
|
||||||
|
|
||||||
|
|
@ -39,7 +52,7 @@ impl Pass for NoisePass {
|
||||||
pass: PassId::Noise,
|
pass: PassId::Noise,
|
||||||
transformed: 0,
|
transformed: 0,
|
||||||
skipped: 0,
|
skipped: 0,
|
||||||
notes: vec!["шум выключен: junk_ratio=0, junk_annotations=0".to_string()],
|
notes: vec!["noise off: junk_ratio=0, junk_annotations=0".to_string()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,45 +63,99 @@ impl Pass for NoisePass {
|
||||||
let real_classes = program.classes.len();
|
let real_classes = program.classes.len();
|
||||||
let count = ratio.saturating_mul(real_classes);
|
let count = ratio.saturating_mul(real_classes);
|
||||||
|
|
||||||
let mut junk_names = Vec::with_capacity(count);
|
let mut class_names =
|
||||||
let mut suffix = 0usize;
|
NameGenerator::new(ctx.rng("noise/classes"), ctx.config.rename.class_style)
|
||||||
for _ in 0..count {
|
.for_classes();
|
||||||
let name = loop {
|
for class in &program.classes {
|
||||||
let candidate = format!("{JUNK_PREFIX}{suffix}");
|
class_names.reserve(class.this_class.clone());
|
||||||
suffix = suffix.saturating_add(1);
|
}
|
||||||
if program.class(&candidate).is_none() {
|
let mut rng = ctx.rng("noise");
|
||||||
break candidate;
|
// (class, entry) pairs for weaving, in creation order.
|
||||||
}
|
let mut entries: Vec<(String, String)> = Vec::with_capacity(count);
|
||||||
};
|
// (class index, method index) of every generated method, for frames.
|
||||||
let junk = ClassBuilder::new(name.clone(), None)
|
let mut fresh_methods: Vec<(usize, usize)> = Vec::new();
|
||||||
.version(version)
|
for index in 0..count {
|
||||||
.method_with_code(ACC_PUBLIC_STATIC, JUNK_ENTRY.0, JUNK_ENTRY.1, empty_code(0))
|
let package = if real_classes > 0 {
|
||||||
.build();
|
let pick = &program.classes[rng.below(real_classes as u64) as usize].this_class;
|
||||||
program.classes.push(junk);
|
parent_package(pick)
|
||||||
junk_names.push(name);
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let simple = class_names.next_name();
|
||||||
|
let name = if package.is_empty() {
|
||||||
|
simple
|
||||||
|
} else {
|
||||||
|
format!("{package}/{simple}")
|
||||||
|
};
|
||||||
|
let mut member_names = NameGenerator::new(
|
||||||
|
ctx.rng_nth("noise/members", index as u64),
|
||||||
|
ctx.config.rename.member_style,
|
||||||
|
);
|
||||||
|
let entry_name = member_names.next_name();
|
||||||
|
let field_name = member_names.next_name();
|
||||||
|
let mut helpers: Vec<(String, String)> = Vec::new();
|
||||||
|
let mut builder = ClassBuilder::new(name.clone(), None).version(version);
|
||||||
|
for _ in 0..methods_per_class {
|
||||||
|
let helper_name = member_names.next_name();
|
||||||
|
let body = helper_body(&mut rng);
|
||||||
|
builder = builder.method_with_code(
|
||||||
|
ACC_PRIVATE_STATIC,
|
||||||
|
helper_name.clone(),
|
||||||
|
HELPER_DESCRIPTOR,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
helpers.push((name.clone(), helper_name));
|
||||||
|
}
|
||||||
|
let callees: Vec<(String, String)> = helpers.clone();
|
||||||
|
let entry = entry_body(&callees, &name, &field_name);
|
||||||
|
builder = builder.method_with_code(
|
||||||
|
ACC_PUBLIC_STATIC,
|
||||||
|
entry_name.clone(),
|
||||||
|
ENTRY_DESCRIPTOR,
|
||||||
|
entry,
|
||||||
|
);
|
||||||
|
let mut junk = builder.build();
|
||||||
|
junk.fields.push(Field {
|
||||||
|
access_flags: ACC_PRIVATE_STATIC,
|
||||||
|
name: field_name,
|
||||||
|
descriptor: FIELD_DESCRIPTOR.to_string(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
});
|
||||||
|
let base = program.classes.len();
|
||||||
|
for mi in 0..junk.methods.len() {
|
||||||
|
fresh_methods.push((base, mi));
|
||||||
|
}
|
||||||
|
program.classes.push(junk);
|
||||||
|
entries.push((name, entry_name));
|
||||||
|
}
|
||||||
|
let added = entries.len();
|
||||||
|
|
||||||
|
// Frames for every generated branching method.
|
||||||
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
|
for (ci, mi) in &fresh_methods {
|
||||||
|
refresh_frames(program, *ci, *mi, &hierarchy)?;
|
||||||
}
|
}
|
||||||
let added = junk_names.len();
|
|
||||||
|
|
||||||
let mut woven = 0usize;
|
let mut woven = 0usize;
|
||||||
if weave && added > 0 {
|
if weave && added > 0 {
|
||||||
woven = weave_first_reachable(program, real_classes, &junk_names)?;
|
woven = weave_first_reachable(program, real_classes, &entries)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut skipped = 0usize;
|
let mut skipped = 0usize;
|
||||||
let mut notes = Vec::new();
|
let mut notes = Vec::new();
|
||||||
if added > 0 {
|
if added > 0 {
|
||||||
notes.push(format!("добавлено junk-классов: {added}"));
|
notes.push(format!("junk classes added: {added}"));
|
||||||
}
|
}
|
||||||
if woven > 0 {
|
if woven > 0 {
|
||||||
notes.push(format!("вплетено вызовов: {woven}"));
|
notes.push(format!("woven calls: {woven}"));
|
||||||
}
|
}
|
||||||
if added == 0 && woven == 0 {
|
if added == 0 && woven == 0 {
|
||||||
notes.push("нечего шуметь: нет классов или методов с кодом".to_string());
|
notes.push("nothing to fuzz: no classes or methods with code".to_string());
|
||||||
}
|
}
|
||||||
if annotations > 0 {
|
if annotations > 0 {
|
||||||
skipped = annotations.saturating_mul(program.classes.len());
|
skipped = annotations.saturating_mul(program.classes.len());
|
||||||
notes.push(format!(
|
notes.push(format!(
|
||||||
"junk_annotations пропущен: сырые атрибуты не могут нести свежие индексы пула (пропущено: {skipped})"
|
"junk_annotations skipped: raw attributes cannot carry fresh pool indices (skipped: {skipped})"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,85 +172,35 @@ impl Pass for NoisePass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
/// Recompute the frames of a generated method.
|
||||||
mod tests {
|
fn refresh_frames(
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
program: &mut Program,
|
||||||
|
ci: usize,
|
||||||
use noxis_classfile::{Insn, InvokeKind, Resource};
|
mi: usize,
|
||||||
use noxis_core::{Config, NoiseConfig};
|
hierarchy: &ClassHierarchy,
|
||||||
|
) -> Result<(), CoreError> {
|
||||||
use super::*;
|
let (name, snapshot) = {
|
||||||
|
let class = program
|
||||||
fn sample() -> Program {
|
.classes
|
||||||
Program {
|
.get(ci)
|
||||||
classes: vec![
|
.ok_or_else(|| CoreError::pass(PassId::Noise, "class vanished"))?;
|
||||||
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
(
|
||||||
.version(Version::JAVA_21)
|
class.this_class.clone(),
|
||||||
.method_with_code(ACC_PUBLIC_STATIC, "run", "()V", empty_code(0))
|
class
|
||||||
.build(),
|
.methods
|
||||||
],
|
.get(mi)
|
||||||
resources: vec![Resource {
|
.cloned()
|
||||||
path: "app.json".into(),
|
.ok_or_else(|| CoreError::pass(PassId::Noise, "method vanished"))?,
|
||||||
data: br#"{}"#.to_vec(),
|
)
|
||||||
}],
|
};
|
||||||
}
|
let frames = compute_frames(&name, &snapshot, hierarchy)
|
||||||
}
|
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
|
||||||
|
program
|
||||||
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
|
.classes
|
||||||
let config = Config {
|
.get_mut(ci)
|
||||||
seed: Some(42),
|
.and_then(|c| c.methods.get_mut(mi))
|
||||||
noise,
|
.and_then(|m| m.code.as_mut())
|
||||||
..Config::default()
|
.ok_or_else(|| CoreError::pass(PassId::Noise, "method vanished"))?
|
||||||
};
|
.frames = frames;
|
||||||
let mut ctx = Context::new(config);
|
Ok(())
|
||||||
NoisePass.apply(&mut ctx, program).expect("noise pass")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn adds_junk_and_weaves_call() {
|
|
||||||
let mut program = sample();
|
|
||||||
let noise = NoiseConfig {
|
|
||||||
junk_ratio: 2,
|
|
||||||
junk_annotations: 0,
|
|
||||||
weave_reachable: true,
|
|
||||||
};
|
|
||||||
let report = run(&mut program, noise);
|
|
||||||
|
|
||||||
assert_eq!(program.classes.len(), 3);
|
|
||||||
assert_eq!(program.classes[1].this_class, "shadow/decoy0");
|
|
||||||
assert_eq!(program.classes[2].this_class, "shadow/decoy1");
|
|
||||||
|
|
||||||
let insns = &program.classes[0].methods[0].code.as_ref().unwrap().insns;
|
|
||||||
assert_eq!(insns.len(), 2);
|
|
||||||
match &insns[0] {
|
|
||||||
Insn::Invoke {
|
|
||||||
kind,
|
|
||||||
owner,
|
|
||||||
name,
|
|
||||||
descriptor,
|
|
||||||
itf,
|
|
||||||
} => {
|
|
||||||
assert_eq!(*kind, InvokeKind::Static);
|
|
||||||
assert_eq!(owner, "shadow/decoy0");
|
|
||||||
assert_eq!(name, "run");
|
|
||||||
assert_eq!(descriptor, "()V");
|
|
||||||
assert!(!itf);
|
|
||||||
}
|
|
||||||
other => panic!("ожидался Invoke, получено: {other:?}"),
|
|
||||||
}
|
|
||||||
assert!(matches!(insns[1], Insn::Return(None)));
|
|
||||||
assert_eq!(report.transformed, 3);
|
|
||||||
assert_eq!(report.skipped, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn disabled_ratio_changes_nothing() {
|
|
||||||
let mut program = sample();
|
|
||||||
let before = program.clone();
|
|
||||||
let report = run(&mut program, NoiseConfig::default());
|
|
||||||
|
|
||||||
assert_eq!(program, before);
|
|
||||||
assert_eq!(report.transformed, 0);
|
|
||||||
assert_eq!(report.skipped, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
208
crates/noxis-passes/src/noise/tests.rs
Normal file
208
crates/noxis-passes/src/noise/tests.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
//! Noise pass tests: junk shape, names, weaving, determinism.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use noxis_classfile::{
|
||||||
|
ClassBuilder, Insn, InvokeKind, Program, Resource, Version, empty_code, format,
|
||||||
|
};
|
||||||
|
use noxis_core::{Config, NoiseConfig};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn sample() -> Program {
|
||||||
|
Program {
|
||||||
|
classes: vec![
|
||||||
|
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.method_with_code(0x0009, "run", "()V", empty_code(0))
|
||||||
|
.build(),
|
||||||
|
],
|
||||||
|
resources: vec![Resource {
|
||||||
|
path: "app.json".into(),
|
||||||
|
data: br#"{}"#.to_vec(),
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noisy() -> NoiseConfig {
|
||||||
|
NoiseConfig {
|
||||||
|
junk_ratio: 2,
|
||||||
|
junk_methods_per_class: 2,
|
||||||
|
junk_annotations: 0,
|
||||||
|
weave_reachable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
|
||||||
|
let config = Config {
|
||||||
|
seed: Some(42),
|
||||||
|
noise,
|
||||||
|
..Config::default()
|
||||||
|
};
|
||||||
|
let mut ctx = Context::new(config);
|
||||||
|
NoisePass.apply(&mut ctx, program).expect("noise pass")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_identifier(name: &str) -> bool {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
match chars.next() {
|
||||||
|
Some(first) if first.is_alphabetic() || first == '_' || first == '$' => {}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adds_junk_with_bodies_and_weaves_call() {
|
||||||
|
let mut program = sample();
|
||||||
|
let report = run(&mut program, noisy());
|
||||||
|
|
||||||
|
assert_eq!(program.classes.len(), 3);
|
||||||
|
let junks = &program.classes[1..];
|
||||||
|
let mut names = Vec::new();
|
||||||
|
for junk in junks {
|
||||||
|
// No self-describing fingerprints, valid identifiers, unique.
|
||||||
|
assert!(
|
||||||
|
!junk.this_class.contains("shadow") && !junk.this_class.contains("decoy"),
|
||||||
|
"fingerprint survived: {}",
|
||||||
|
junk.this_class
|
||||||
|
);
|
||||||
|
for part in junk.this_class.split('/') {
|
||||||
|
assert!(is_identifier(part), "bad segment in {}", junk.this_class);
|
||||||
|
}
|
||||||
|
names.push(junk.this_class.clone());
|
||||||
|
// One accumulator field, fed by the entry.
|
||||||
|
assert_eq!(junk.fields.len(), 1);
|
||||||
|
assert_eq!(junk.fields[0].descriptor, "I");
|
||||||
|
let field = junk.fields[0].name.clone();
|
||||||
|
// Helpers have real branching bodies, not bare returns.
|
||||||
|
let helpers: Vec<_> = junk
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.descriptor == HELPER_DESCRIPTOR)
|
||||||
|
.collect();
|
||||||
|
assert_eq!(helpers.len(), 2);
|
||||||
|
for helper in helpers {
|
||||||
|
let code = helper.code.as_ref().expect("code");
|
||||||
|
assert!(
|
||||||
|
code.insns.iter().any(|i| matches!(i, Insn::Branch(..))),
|
||||||
|
"helper without branches"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
code.insns.iter().any(|i| matches!(i, Insn::Binary { .. })),
|
||||||
|
"helper without arithmetic"
|
||||||
|
);
|
||||||
|
assert!(!code.frames.is_empty(), "helper frames were not computed");
|
||||||
|
}
|
||||||
|
// The entry calls the helpers and stores into the field.
|
||||||
|
let entry = junk
|
||||||
|
.methods
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.descriptor == ENTRY_DESCRIPTOR)
|
||||||
|
.expect("entry");
|
||||||
|
let entry_code = entry.code.as_ref().expect("code");
|
||||||
|
let calls = entry_code
|
||||||
|
.insns
|
||||||
|
.iter()
|
||||||
|
.filter(|i| matches!(i, Insn::Invoke { .. }))
|
||||||
|
.count();
|
||||||
|
assert_eq!(calls, 2, "entry must call every helper");
|
||||||
|
assert!(
|
||||||
|
entry_code
|
||||||
|
.insns
|
||||||
|
.iter()
|
||||||
|
.any(|i| matches!(i, Insn::PutStatic { name, .. } if name == &field)),
|
||||||
|
"entry must feed the field"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_ne!(names[0], names[1]);
|
||||||
|
|
||||||
|
// The live method opens with a call into the first junk entry.
|
||||||
|
let live = program.classes[0].methods[0].code.as_ref().unwrap();
|
||||||
|
match &live.insns[0] {
|
||||||
|
Insn::Invoke {
|
||||||
|
kind,
|
||||||
|
owner,
|
||||||
|
descriptor,
|
||||||
|
itf,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(*kind, InvokeKind::Static);
|
||||||
|
assert_eq!(owner, &names[0]);
|
||||||
|
assert_eq!(descriptor, "()V");
|
||||||
|
assert!(!itf);
|
||||||
|
}
|
||||||
|
other => panic!("expected Invoke, got: {other:?}"),
|
||||||
|
}
|
||||||
|
assert_eq!(report.transformed, 3);
|
||||||
|
assert_eq!(report.skipped, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_ratio_changes_nothing() {
|
||||||
|
let mut program = sample();
|
||||||
|
let before = program.clone();
|
||||||
|
let report = run(&mut program, NoiseConfig::default());
|
||||||
|
|
||||||
|
assert_eq!(program, before);
|
||||||
|
assert_eq!(report.transformed, 0);
|
||||||
|
assert_eq!(report.skipped, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn junk_is_deterministic() {
|
||||||
|
let mut first = sample();
|
||||||
|
let mut second = sample();
|
||||||
|
run(&mut first, noisy());
|
||||||
|
run(&mut second, noisy());
|
||||||
|
assert_eq!(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Junk bodies carry real branches: they must verify under a real JVM.
|
||||||
|
#[test]
|
||||||
|
fn junk_verifies_under_java() {
|
||||||
|
if std::process::Command::new("java")
|
||||||
|
.arg("-version")
|
||||||
|
.output()
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
eprintln!("SKIP: no java");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut program = sample();
|
||||||
|
run(
|
||||||
|
&mut program,
|
||||||
|
NoiseConfig {
|
||||||
|
junk_ratio: 1,
|
||||||
|
..noisy()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let dir = std::env::temp_dir().join(format!("noxis-noise-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
for class in &program.classes {
|
||||||
|
let bytes = format::write(class).expect("write");
|
||||||
|
let mut path = dir.clone();
|
||||||
|
for part in class.this_class.split('/') {
|
||||||
|
path.push(part);
|
||||||
|
}
|
||||||
|
path.set_extension("class");
|
||||||
|
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
|
||||||
|
std::fs::write(&path, bytes).expect("write class");
|
||||||
|
}
|
||||||
|
for class in &program.classes {
|
||||||
|
let dotted = class.this_class.replace('/', ".");
|
||||||
|
let output = std::process::Command::new("java")
|
||||||
|
.arg("-Xverify:all")
|
||||||
|
.arg("-cp")
|
||||||
|
.arg(&dir)
|
||||||
|
.arg(&dotted)
|
||||||
|
.output()
|
||||||
|
.expect("run java");
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
!stderr.contains("VerifyError") && !stderr.contains("ClassFormatError"),
|
||||||
|
"{dotted}: {stderr}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
//! Вплетание junk-классов в граф достижимости живых методов.
|
//! Weaving junk classes into the reachability graph of live methods.
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::ClassHierarchy;
|
||||||
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
|
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
|
||||||
use noxis_core::{CoreError, PassId};
|
use noxis_core::{CoreError, PassId};
|
||||||
|
|
||||||
use super::JUNK_ENTRY;
|
/// The woven call descriptor (entries are always `()V`).
|
||||||
|
const WOVEN_DESCRIPTOR: &str = "()V";
|
||||||
|
|
||||||
/// Вплетает вызов первого junk-класса в первый метод с кодом.
|
/// Weave a call of the first junk entry into the first method with code.
|
||||||
///
|
///
|
||||||
/// Вызов встаёт перед терминалом (`Return`/`AReturn`/`AThrow`), чтобы не
|
/// The call lands before the terminal (`Return`/`AReturn`/`AThrow`) so it
|
||||||
/// породить недостижимый хвост без фреймов; фреймы пересчитываются.
|
/// never strands an unreachable tail without frames; frames are recomputed.
|
||||||
pub(super) fn weave_first_reachable(
|
pub(super) fn weave_first_reachable(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
real_classes: usize,
|
real_classes: usize,
|
||||||
junk_names: &[String],
|
entries: &[(String, String)],
|
||||||
) -> Result<usize, CoreError> {
|
) -> Result<usize, CoreError> {
|
||||||
let missing = || CoreError::pass(PassId::Noise, "цель вплетания пропала");
|
let missing = || CoreError::pass(PassId::Noise, "weave target vanished");
|
||||||
let Some(owner) = junk_names.first() else {
|
let Some((owner, entry)) = entries.first() else {
|
||||||
return Err(missing());
|
return Err(missing());
|
||||||
};
|
};
|
||||||
let target = program.classes[..real_classes.min(program.classes.len())]
|
let target = program.classes[..real_classes.min(program.classes.len())]
|
||||||
|
|
@ -39,8 +40,8 @@ pub(super) fn weave_first_reachable(
|
||||||
let call = Insn::Invoke {
|
let call = Insn::Invoke {
|
||||||
kind: InvokeKind::Static,
|
kind: InvokeKind::Static,
|
||||||
owner: owner.clone(),
|
owner: owner.clone(),
|
||||||
name: JUNK_ENTRY.0.to_string(),
|
name: entry.clone(),
|
||||||
descriptor: JUNK_ENTRY.1.to_string(),
|
descriptor: WOVEN_DESCRIPTOR.to_string(),
|
||||||
itf: false,
|
itf: false,
|
||||||
};
|
};
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ fn params_of(descriptor: &str) -> Option<String> {
|
||||||
Some(descriptor[..=close].to_string())
|
Some(descriptor[..=close].to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parent_package(internal: &str) -> String {
|
pub(crate) fn parent_package(internal: &str) -> String {
|
||||||
match internal.rfind('/') {
|
match internal.rfind('/') {
|
||||||
Some(i) => internal[..i].to_string(),
|
Some(i) => internal[..i].to_string(),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
//! Пасс переименования: классы, поля, методы, локальные переменные, дескрипторы.
|
//! The rename pass: classes, fields, methods, locals, descriptors.
|
||||||
//!
|
//!
|
||||||
//! Это самая дешёвая и самая отдающая техника (см. `info/evo-weaknesses.md`:
|
//! The cheapest technique with the highest payoff (see `info/evo-weaknesses.md`:
|
||||||
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
|
//! EvoVis never renamed members at all, so the class read plainly via `javap -p`).
|
||||||
//!
|
//!
|
||||||
//! Тонкости, которые здесь учитываются:
|
//! Subtleties handled here:
|
||||||
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс - нужны
|
//! - a `Sub.staticMethod()` call may name a subclass as the owner - aliases
|
||||||
//! псевдонимы (`alias`) для всей иерархии;
|
//! (`alias`) are needed across the hierarchy;
|
||||||
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
|
//! - bridge methods (`ACC_BRIDGE`) and covariant returns live in one method
|
||||||
//! «семействе» метода - переименовывать их нужно разом;
|
//! "family" - they must be renamed together;
|
||||||
//! - `serialVersionUID`, JNI, рефлексия, сериализация - не трогаем.
|
//! - `serialVersionUID`, JNI, reflection, serialization stay untouched.
|
||||||
|
|
||||||
/// Карты и стратегия назначения имён.
|
/// Name maps and the assignment strategy.
|
||||||
mod assign;
|
pub(crate) mod assign;
|
||||||
/// Переписывание тел методов и атрибутов.
|
/// Rewriting method bodies and attributes.
|
||||||
mod rewrite;
|
mod rewrite;
|
||||||
/// Интеграционные тесты пасса.
|
/// Pass integration tests.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ const ACC_STATIC: u16 = 0x0008;
|
||||||
const ACC_FINAL: u16 = 0x0010;
|
const ACC_FINAL: u16 = 0x0010;
|
||||||
const ACC_ENUM: u16 = 0x4000;
|
const ACC_ENUM: u16 = 0x4000;
|
||||||
|
|
||||||
/// Пасс переименования.
|
/// The rename pass.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct RenamePass;
|
pub struct RenamePass;
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ impl Pass for RenamePass {
|
||||||
let mut maps = RenameMaps::default();
|
let mut maps = RenameMaps::default();
|
||||||
let mut renamed_classes = 0usize;
|
let mut renamed_classes = 0usize;
|
||||||
|
|
||||||
// 1. Имена классов.
|
// 1. Class names.
|
||||||
{
|
{
|
||||||
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
|
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
|
||||||
let mut names =
|
let mut names =
|
||||||
|
|
@ -91,7 +91,7 @@ impl Pass for RenamePass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Простые (не-виртуальные) члены: private/static/final.
|
// 2. Simple (non-virtual) members: private/static/final.
|
||||||
let mut renamed_methods = 0usize;
|
let mut renamed_methods = 0usize;
|
||||||
let mut renamed_fields = 0usize;
|
let mut renamed_fields = 0usize;
|
||||||
|
|
||||||
|
|
@ -138,8 +138,8 @@ impl Pass for RenamePass {
|
||||||
);
|
);
|
||||||
renamed_methods += 1;
|
renamed_methods += 1;
|
||||||
}
|
}
|
||||||
// Виртуальный не-final: переименовывается только вместе
|
// Non-final virtual: renamed only together with the whole
|
||||||
// со всем семейством (см. rename_virtual_families).
|
// family (see rename_virtual_families).
|
||||||
_ => method_names.reserve(method.name.clone()),
|
_ => method_names.reserve(method.name.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -179,28 +179,28 @@ impl Pass for RenamePass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Виртуальные семейства - только в агрессивном режиме.
|
// 3. Virtual families - aggressive mode only.
|
||||||
if cfg.rename.rename_public_members {
|
if cfg.rename.rename_public_members {
|
||||||
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Псевдоними для наследников.
|
// 4. Aliases for subclasses.
|
||||||
maps.expand(&hierarchy, &owners);
|
maps.expand(&hierarchy, &owners);
|
||||||
|
|
||||||
// 5. Переписать определения и все ссылки.
|
// 5. Rewrite definitions and all references.
|
||||||
let mut local_counter = 0u64;
|
let mut local_counter = 0u64;
|
||||||
for class in &mut program.classes {
|
for class in &mut program.classes {
|
||||||
let orig_owner = class.this_class.clone();
|
let orig_owner = class.this_class.clone();
|
||||||
rewrite_class(class, &orig_owner, &maps, &cfg, &mut local_counter, ctx);
|
rewrite_class(class, &orig_owner, &maps, &cfg, &mut local_counter, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Ширинирование доступа: после переезда в другие пакеты иначе
|
// 6. Access widening: moving across packages could otherwise
|
||||||
// возможен IllegalAccessError.
|
// cause IllegalAccessError.
|
||||||
if cfg.rename.widen_access {
|
if cfg.rename.widen_access {
|
||||||
widen_all(program);
|
widen_all(program);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Маппинги в отчёт (обратная деобфускация stack trace'ей).
|
// 7. Mappings for the report (stack-trace deobfuscation).
|
||||||
for ((owner, name, desc), new_name) in maps.methods.iter() {
|
for ((owner, name, desc), new_name) in maps.methods.iter() {
|
||||||
ctx.mapping
|
ctx.mapping
|
||||||
.methods
|
.methods
|
||||||
|
|
@ -217,7 +217,7 @@ impl Pass for RenamePass {
|
||||||
|
|
||||||
let locked = analysis.locked_classes.len();
|
let locked = analysis.locked_classes.len();
|
||||||
if locked > 0 {
|
if locked > 0 {
|
||||||
ctx.note(format!("rename: {locked} классов в keep (не тронуты)"));
|
ctx.note(format!("rename: {locked} keep-listed classes (untouched)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(PassReport {
|
Ok(PassReport {
|
||||||
|
|
@ -225,7 +225,7 @@ impl Pass for RenamePass {
|
||||||
transformed: renamed_classes + renamed_methods + renamed_fields,
|
transformed: renamed_classes + renamed_methods + renamed_fields,
|
||||||
skipped: locked,
|
skipped: locked,
|
||||||
notes: vec![format!(
|
notes: vec![format!(
|
||||||
"классов: {renamed_classes}, методов: {renamed_methods}, полей: {renamed_fields}"
|
"classes: {renamed_classes}, methods: {renamed_methods}, fields: {renamed_fields}"
|
||||||
)],
|
)],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
use noxis_classfile::{Const, Insn, InvokeKind};
|
use noxis_classfile::{Const, Insn, InvokeKind};
|
||||||
use noxis_runtime::{DECODER_DESC, DECODER_METHOD};
|
use noxis_runtime::{DECODER_DESC, DECODER_METHOD};
|
||||||
|
|
||||||
/// Разбить строку на чанки по границе символов (1–3 штуки).
|
/// Разбить строку на чанки по границе символов (1-3 штуки).
|
||||||
pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
|
pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
|
||||||
let chars: Vec<char> = text.chars().collect();
|
let chars: Vec<char> = text.chars().collect();
|
||||||
if chars.len() <= split_above.max(1) {
|
if chars.len() <= split_above.max(1) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue