obfuskator/crates/noxis-analysis/tests/dto.rs
loki5512344 c3acc0baf4
analysis: lock DTO fields passed as X.class to databind calls
Gson/Jackson/fastjson2/Moshi bind fields by reflection from a Class
literal, with no field-name string for the string-based detection:
renaming such a field silently breaks deserialization. Absorb_databind
finds the nearest Const::Class before fromJson/readValue/parseObject/
adapter calls (10-insn window, control-flow boundaries stop the search)
and keeps every field of that class; the class itself still renames.

Tests: noxis-analysis/tests/dto.rs (red first), keep unit tests moved
to tests/keep.rs (file budget). Code comments migrated to English.
2026-10-09 13:51:35 +02:00

70 lines
2.3 KiB
Rust

//! Databinding keep: `X.class` passed to Gson/Jackson locks the fields of `X`.
//!
//! JSON libraries bind fields by reflection from a `Class` literal, with no
//! field-name string in the bytecode for the existing string-based detection
//! to catch. Renaming such a field silently breaks deserialization, so every
//! field of the literal's class stays.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_analysis::Analysis;
use noxis_classfile::{ClassBuilder, Code, Const, Field, Insn, InvokeKind, Program, Version};
use noxis_core::Config;
/// `gson.fromJson(reader, State.class)`: `State` has a private field.
fn sample() -> Program {
let caller = Code {
max_stack: 3,
max_locals: 2,
insns: vec![
Insn::Load {
slot: 1,
kind: noxis_classfile::SlotKind::Ref,
},
Insn::Const(Const::Class("demo/State".to_string())),
Insn::Invoke {
kind: InvokeKind::Virtual,
owner: "com/google/gson/Gson".to_string(),
name: "fromJson".to_string(),
descriptor: "(Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object;".to_string(),
itf: false,
},
Insn::Pop,
Insn::Return(None),
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
let mut state = ClassBuilder::new("demo/State", Some("java/lang/Object"))
.version(Version::JAVA_21)
.build();
state.fields.push(Field {
access_flags: 0x0002,
name: "nickname".to_string(),
descriptor: "Ljava/lang/String;".to_string(),
attributes: Vec::new(),
});
let app = ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0009, "load", "()V", caller)
.build();
Program {
classes: vec![state, app],
resources: Vec::new(),
}
}
#[test]
fn databind_class_literal_locks_fields() {
let program = sample();
let analysis = Analysis::run(&program, &Config::default()).expect("analysis");
assert!(
analysis.keep.keeps_member("nickname"),
"DTO field must survive renaming"
);
assert!(
!analysis.keep.keeps_member("load"),
"unrelated members must still rename"
);
}