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"
|
||||
);
|
||||
}
|
||||
33
crates/noxis-analysis/tests/keep.rs
Normal file
33
crates/noxis-analysis/tests/keep.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
//! Keep-rule unit tests: globs and exact names.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use noxis_analysis::KeepRules;
|
||||
use noxis_core::Config;
|
||||
|
||||
fn rules_with_patterns() -> KeepRules {
|
||||
KeepRules::from_config(&Config {
|
||||
keep: vec!["com/example/*".to_string(), "**/internal/**".to_string()],
|
||||
keep_resources: vec!["META-INF/services/*".to_string()],
|
||||
..Config::default()
|
||||
})
|
||||
.expect("patterns")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob_semantics() {
|
||||
let rules = rules_with_patterns();
|
||||
assert!(rules.keeps_class("com/example/Main"));
|
||||
assert!(!rules.keeps_class("com/example/sub/Main"));
|
||||
assert!(rules.keeps_class("x/internal/y/Z"));
|
||||
assert!(rules.keeps_resource("META-INF/services/a.b.C"));
|
||||
assert!(!rules.keeps_resource("assets/mc/mod.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_names_win() {
|
||||
let mut rules = KeepRules::default();
|
||||
rules.keep_exact("app/Main");
|
||||
assert!(rules.keeps_class("app/Main"));
|
||||
assert!(!rules.keeps_class("app/Other"));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue