From 74ad4552e4312f53e20fc68ca92d6fd2b2c387de Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 9 Oct 2026 13:53:30 +0200 Subject: [PATCH] analysis,verify: databind field lock and locked-class share KeepRules::absorb_databind finds X.class literals before Gson / Jackson / fastjson2 / Moshi calls and locks every field of X (reflection-bound names never reach the string-based detection). The class itself still renames. Test in analysis/tests/dto.rs; keep unit tests moved to tests/keep.rs. ProgramStats gains locked_classes + locked_share() so analyst-bait source names are loud in the report instead of surfacing later. --- crates/noxis-verify/src/metrics.rs | 67 +++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/crates/noxis-verify/src/metrics.rs b/crates/noxis-verify/src/metrics.rs index 958c6a3..93e5122 100644 --- a/crates/noxis-verify/src/metrics.rs +++ b/crates/noxis-verify/src/metrics.rs @@ -1,10 +1,11 @@ -//! Метрики программы: счётчики, раздувание, стабильность маппингов. +//! Program metrics: counters, inflation, mapping stability. +use noxis_analysis::Analysis; use noxis_classfile::{Const, Insn, Program}; use noxis_core::Mapping; use serde::Serialize; -/// Счётчики программы (для отчёта и бюджетов). +/// Program counters (for the report and budgets). #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] pub struct ProgramStats { pub classes: usize, @@ -13,16 +14,32 @@ pub struct ProgramStats { pub insns: usize, pub strings: usize, pub resources: usize, + /// Classes kept under their source name (entrypoints, reflection, config). + /// They stick out next to obfuscated neighbours, so the share is loud. + pub locked_classes: usize, } -/// Посчитать статистику по программе. -pub fn program_stats(program: &Program) -> ProgramStats { +impl ProgramStats { + /// The locked share of all classes (`0.0` when there are none). + pub fn locked_share(&self) -> f64 { + if self.classes == 0 { + return 0.0; + } + self.locked_classes as f64 / self.classes as f64 + } +} + +/// Count the program stats, including the locked-class share. +pub fn program_stats(program: &Program, analysis: &Analysis) -> ProgramStats { let mut stats = ProgramStats { classes: program.classes.len(), resources: program.resources.len(), ..ProgramStats::default() }; for class in &program.classes { + if analysis.is_locked(&class.this_class) { + stats.locked_classes += 1; + } stats.fields += class.fields.len(); stats.methods += class.methods.len(); for method in &class.methods { @@ -40,7 +57,7 @@ pub fn program_stats(program: &Program) -> ProgramStats { stats } -/// Коэффициент раздувания (`after / before`). +/// The inflation factor (`after / before`). pub fn inflation_ratio(before_bytes: u64, after_bytes: u64) -> f64 { if before_bytes == 0 { return f64::INFINITY; @@ -48,18 +65,16 @@ pub fn inflation_ratio(before_bytes: u64, after_bytes: u64) -> f64 { after_bytes as f64 / before_bytes as f64 } -/// Раздувание в бюджете (`max_ratio`, например `1.5` = +50%). +/// Inflation within budget (`max_ratio`, e.g. `1.5` = +50%). pub fn check_inflation(before_bytes: u64, after_bytes: u64, max_ratio: f64) -> Result<(), String> { let ratio = inflation_ratio(before_bytes, after_bytes); if ratio > max_ratio { - return Err(format!( - "раздувание {ratio:.2} сверх бюджета {max_ratio:.2}" - )); + return Err(format!("inflation {ratio:.2} over budget {max_ratio:.2}")); } Ok(()) } -/// Маппинги обязаны меняться между сборками (иначе сид не работает). +/// Mappings must change between builds (otherwise the seed does nothing). pub fn mappings_changed(before: &Mapping, after: &Mapping) -> bool { before != after } @@ -72,7 +87,7 @@ mod tests { #[test] fn empty_program_stats_are_zero() { - let stats = program_stats(&Program::default()); + let stats = program_stats(&Program::default(), &empty_analysis()); assert_eq!(stats, ProgramStats::default()); assert_eq!(stats.classes, 0); } @@ -90,4 +105,34 @@ mod tests { let mapping = Mapping::default(); assert!(!mappings_changed(&mapping, &mapping)); } + + /// Locked classes stick out next to obfuscated neighbours: count them. + #[test] + fn locked_classes_are_counted() { + use noxis_analysis::Analysis; + use noxis_classfile::ClassBuilder; + use noxis_core::Config; + + let program = Program { + classes: vec![ + ClassBuilder::new("app/Keep", None).build(), + ClassBuilder::new("app/Hide", None).build(), + ], + resources: Vec::new(), + }; + let config = Config { + keep: vec!["app/Keep".to_string()], + ..Config::default() + }; + let analysis = Analysis::run(&program, &config).expect("analysis"); + let stats = program_stats(&program, &analysis); + assert_eq!(stats.classes, 2); + assert_eq!(stats.locked_classes, 1); + assert!((stats.locked_share() - 0.5).abs() < f64::EPSILON); + } + + fn empty_analysis() -> noxis_analysis::Analysis { + noxis_analysis::Analysis::run(&Program::default(), &noxis_core::Config::default()) + .expect("empty analysis") + } }