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_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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue