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.
This commit is contained in:
parent
c3acc0baf4
commit
74ad4552e4
1 changed files with 56 additions and 11 deletions
|
|
@ -1,10 +1,11 @@
|
||||||
//! Метрики программы: счётчики, раздувание, стабильность маппингов.
|
//! Program metrics: counters, inflation, mapping stability.
|
||||||
|
|
||||||
|
use noxis_analysis::Analysis;
|
||||||
use noxis_classfile::{Const, Insn, Program};
|
use noxis_classfile::{Const, Insn, Program};
|
||||||
use noxis_core::Mapping;
|
use noxis_core::Mapping;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// Счётчики программы (для отчёта и бюджетов).
|
/// Program counters (for the report and budgets).
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||||
pub struct ProgramStats {
|
pub struct ProgramStats {
|
||||||
pub classes: usize,
|
pub classes: usize,
|
||||||
|
|
@ -13,16 +14,32 @@ pub struct ProgramStats {
|
||||||
pub insns: usize,
|
pub insns: usize,
|
||||||
pub strings: usize,
|
pub strings: usize,
|
||||||
pub resources: 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Посчитать статистику по программе.
|
impl ProgramStats {
|
||||||
pub fn program_stats(program: &Program) -> 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 {
|
let mut stats = ProgramStats {
|
||||||
classes: program.classes.len(),
|
classes: program.classes.len(),
|
||||||
resources: program.resources.len(),
|
resources: program.resources.len(),
|
||||||
..ProgramStats::default()
|
..ProgramStats::default()
|
||||||
};
|
};
|
||||||
for class in &program.classes {
|
for class in &program.classes {
|
||||||
|
if analysis.is_locked(&class.this_class) {
|
||||||
|
stats.locked_classes += 1;
|
||||||
|
}
|
||||||
stats.fields += class.fields.len();
|
stats.fields += class.fields.len();
|
||||||
stats.methods += class.methods.len();
|
stats.methods += class.methods.len();
|
||||||
for method in &class.methods {
|
for method in &class.methods {
|
||||||
|
|
@ -40,7 +57,7 @@ pub fn program_stats(program: &Program) -> ProgramStats {
|
||||||
stats
|
stats
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Коэффициент раздувания (`after / before`).
|
/// The inflation factor (`after / before`).
|
||||||
pub fn inflation_ratio(before_bytes: u64, after_bytes: u64) -> f64 {
|
pub fn inflation_ratio(before_bytes: u64, after_bytes: u64) -> f64 {
|
||||||
if before_bytes == 0 {
|
if before_bytes == 0 {
|
||||||
return f64::INFINITY;
|
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
|
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> {
|
pub fn check_inflation(before_bytes: u64, after_bytes: u64, max_ratio: f64) -> Result<(), String> {
|
||||||
let ratio = inflation_ratio(before_bytes, after_bytes);
|
let ratio = inflation_ratio(before_bytes, after_bytes);
|
||||||
if ratio > max_ratio {
|
if ratio > max_ratio {
|
||||||
return Err(format!(
|
return Err(format!("inflation {ratio:.2} over budget {max_ratio:.2}"));
|
||||||
"раздувание {ratio:.2} сверх бюджета {max_ratio:.2}"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Маппинги обязаны меняться между сборками (иначе сид не работает).
|
/// Mappings must change between builds (otherwise the seed does nothing).
|
||||||
pub fn mappings_changed(before: &Mapping, after: &Mapping) -> bool {
|
pub fn mappings_changed(before: &Mapping, after: &Mapping) -> bool {
|
||||||
before != after
|
before != after
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +87,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_program_stats_are_zero() {
|
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, ProgramStats::default());
|
||||||
assert_eq!(stats.classes, 0);
|
assert_eq!(stats.classes, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -90,4 +105,34 @@ mod tests {
|
||||||
let mapping = Mapping::default();
|
let mapping = Mapping::default();
|
||||||
assert!(!mappings_changed(&mapping, &mapping));
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue