diff --git a/crates/noxis-verify/src/java.rs b/crates/noxis-verify/src/java.rs index f83fd65..1c5c2f7 100644 --- a/crates/noxis-verify/src/java.rs +++ b/crates/noxis-verify/src/java.rs @@ -27,6 +27,96 @@ pub fn java_available() -> bool { .is_ok_and(|out| out.status.success()) } +/// Whether `jadx` is available for decompile checks. +pub fn jadx_available() -> bool { + Command::new("jadx") + .arg("--version") + .output() + .is_ok_and(|out| out.status.success()) +} + +/// Decompile quality of one jar: how much of the output jadx itself flags. +/// +/// `JADX WARN` markers mean recovered-but-dubious structures; `Failed` +/// markers mean unrecovered ones. Obfuscation wins when these climb while +/// `java -Xverify:all` stays green: the bytecode runs, but readers drown. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DecompileReport { + /// `.java` files emitted. + pub files: usize, + /// Files with at least one `JADX WARN`. + pub files_with_warn: usize, + /// Total `JADX WARN` markers. + pub warns: usize, + /// `JADX WARN: Failed...` markers (unrecovered structures). + pub failed: usize, + /// Warn markers by kind (first word after the prefix). + pub kinds: std::collections::BTreeMap, + /// jadx exit code. + pub exit_code: Option, +} + +/// Decompile a jar with jadx into a scratch dir and grade the output. +/// `None` when jadx is missing or refuses to start. +pub fn decompile_report(jar: &Path) -> Option { + if !jadx_available() { + return None; + } + let scratch = std::env::temp_dir().join(format!("noxis-jadx-rep-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&scratch); + let output = Command::new("jadx") + .arg("-q") + .arg("-d") + .arg(&scratch) + .arg(jar) + .output() + .ok()?; + let mut report = DecompileReport { + exit_code: output.status.code(), + ..DecompileReport::default() + }; + let mut stack = vec![scratch.clone()]; + while let Some(dir) = stack.pop() { + let Ok(entries) = std::fs::read_dir(&dir) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + stack.push(path); + } else if path.extension().is_some_and(|ext| ext == "java") { + report.files += 1; + let text = std::fs::read(&path) + .map(|bytes| String::from_utf8_lossy(&bytes).into_owned()) + .unwrap_or_default(); + let mut file_warns = 0; + for line in text.lines() { + if let Some(rest) = line.find("JADX WARN:").map(|i| &line[i + 10..]) { + let kind: String = rest + .trim_start_matches([' ', '\'', '"']) + .split_whitespace() + .next() + .unwrap_or("?") + .trim_matches(['\'', '"', ':']) + .to_string(); + *report.kinds.entry(kind.clone()).or_insert(0) += 1; + if kind == "Failed" { + report.failed += 1; + } + report.warns += 1; + file_warns += 1; + } + } + if file_warns > 0 { + report.files_with_warn += 1; + } + } + } + } + let _ = std::fs::remove_dir_all(&scratch); + Some(report) +} + /// Проверить байты одного класса настоящим верификатором. pub fn verify_class_bytes(internal_name: &str, bytes: &[u8]) -> JavaVerdict { if !java_available() { diff --git a/crates/noxis-verify/src/lib.rs b/crates/noxis-verify/src/lib.rs index 98dfa08..431920f 100644 --- a/crates/noxis-verify/src/lib.rs +++ b/crates/noxis-verify/src/lib.rs @@ -10,7 +10,10 @@ pub mod java; pub mod leaks; pub mod metrics; -pub use java::{JavaVerdict, java_available, verify_class_bytes, verify_jar}; +pub use java::{ + DecompileReport, JavaVerdict, decompile_report, jadx_available, java_available, + verify_class_bytes, verify_jar, +}; pub use leaks::{RED_PATTERNS, SecretHit, check_patterns, collect_names, find_leaks, scan_secrets}; pub use metrics::{ ProgramStats, check_inflation, inflation_ratio, mappings_changed, program_stats, diff --git a/crates/noxis-verify/tests/decompile.rs b/crates/noxis-verify/tests/decompile.rs new file mode 100644 index 0000000..f62caff --- /dev/null +++ b/crates/noxis-verify/tests/decompile.rs @@ -0,0 +1,108 @@ +//! Decompile-quality metric: jadx must chew a clean jar without warnings. + +#![allow(clippy::expect_used, clippy::unwrap_used)] + +use noxis_classfile::{ClassBuilder, Code, Insn, Version}; +use noxis_verify::{decompile_report, jadx_available}; + +/// Two classes, one with a branchy method: enough structure for jadx to +/// prove it can read the jar, small enough to run in milliseconds. +fn sample_jar(path: &std::path::Path) { + let loop_code = Code { + max_stack: 2, + max_locals: 2, + insns: vec![ + Insn::Const(noxis_classfile::Const::Int(0)), + Insn::Store { + slot: 0, + kind: noxis_classfile::SlotKind::Int, + }, + Insn::Mark(0), + Insn::Load { + slot: 0, + kind: noxis_classfile::SlotKind::Int, + }, + Insn::Const(noxis_classfile::Const::Int(10)), + Insn::Branch( + noxis_classfile::BranchCond::Int(noxis_classfile::Cond::Ge), + 1, + ), + Insn::Load { + slot: 0, + kind: noxis_classfile::SlotKind::Int, + }, + Insn::Const(noxis_classfile::Const::Int(1)), + Insn::Binary { + prim: noxis_classfile::Prim::Int, + op: noxis_classfile::Arith::Add, + }, + Insn::Store { + slot: 0, + kind: noxis_classfile::SlotKind::Int, + }, + Insn::Goto(0), + Insn::Mark(1), + Insn::Load { + slot: 0, + kind: noxis_classfile::SlotKind::Int, + }, + Insn::Return(Some(noxis_classfile::Prim::Int)), + ], + exception_table: Vec::new(), + frames: Vec::new(), + attributes: Vec::new(), + }; + let mut program = noxis_classfile::Program { + classes: vec![ + ClassBuilder::new("demo/App", Some("java/lang/Object")) + .version(Version::JAVA_21) + .method_with_code(0x0009, "run", "()I", loop_code) + .build(), + ClassBuilder::new("demo/Help", Some("java/lang/Object")) + .version(Version::JAVA_21) + .build(), + ], + resources: Vec::new(), + }; + // Real frames: jadx grades structure, not just parsing. + let hierarchy = noxis_analysis::ClassHierarchy::build(&program); + for class in &mut program.classes { + for method in &mut class.methods { + if method.code.is_none() { + continue; + } + let snapshot = method.clone(); + let frames = noxis_classfile::compute_frames(&class.this_class, &snapshot, &hierarchy) + .expect("frames"); + if let Some(code) = method.code.as_mut() { + code.frames = frames; + } + } + } + let entries = noxis_jar::save(&program).expect("save"); + noxis_jar::write_file(path, &entries).expect("write jar"); +} + +#[test] +fn clean_jar_decompiles_without_warnings() { + if !jadx_available() { + eprintln!("SKIP: no jadx"); + return; + } + let dir = std::env::temp_dir().join(format!("noxis-jadx-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("mkdir"); + let jar = dir.join("in.jar"); + sample_jar(&jar); + let report = decompile_report(&jar).expect("jadx runs"); + assert!( + report.files >= 2, + "expected 2 classes, got {}", + report.files + ); + assert_eq!(report.failed, 0, "clean code must not fail: {report:?}"); + assert_eq!( + report.files_with_warn, 0, + "clean code must not warn: {report:?}" + ); +} diff --git a/crates/noxis/src/cmd/analyze.rs b/crates/noxis/src/cmd/analyze.rs index 0750021..895f4fc 100644 --- a/crates/noxis/src/cmd/analyze.rs +++ b/crates/noxis/src/cmd/analyze.rs @@ -14,6 +14,9 @@ pub struct Args { /// TOML-конфиг с keep-паттернами (без него - пустой keep). #[arg(short, long)] pub config: Option, + /// Декомпилировать jadx и показать долю распознанных структур. + #[arg(long)] + pub decompile: bool, } /// Показать классы, entrypoint'ы и keep-правила. @@ -48,6 +51,22 @@ pub fn run(args: Args) -> Result<()> { analysis.entrypoints.reflective_names.len() ); println!("keep-правил: {}", analysis.keep.len()); + if args.decompile { + match noxis_verify::decompile_report(&args.input) { + None => println!("decompile: нет jadx"), + Some(report) => { + println!( + "decompile: файлов {}, с WARN {}, WARN всего {}, Failed {}", + report.files, report.files_with_warn, report.warns, report.failed + ); + let mut kinds: Vec<(&String, &usize)> = report.kinds.iter().collect(); + kinds.sort_by(|a, b| b.1.cmp(a.1)); + for (kind, count) in kinds.iter().take(8) { + println!(" {kind}: {count}"); + } + } + } + } Ok(()) } @@ -62,6 +81,7 @@ mod tests { let args = Args { input: PathBuf::from("definitely-not-here.jar"), config: None, + decompile: false, }; assert!(run(args).is_err()); }