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.
This commit is contained in:
parent
139aea9a8f
commit
c3acc0baf4
3 changed files with 215 additions and 61 deletions
70
crates/noxis-analysis/tests/dto.rs
Normal file
70
crates/noxis-analysis/tests/dto.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//! 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"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue